Mobile Development - 'Support Side Story'

Broadcasting technical support to Windows Mobile\CE Application Developers to help realizing their potential

Broadcasting technical support to Windows Mobile\CE Application Developers to help realizing their potential

February, 2008

  • Mobile Development - 'Support Side Story'

    Support Boundaries for Windows Mobile Programming (Developing Drivers, for example... Or even WiFi Programming)

    • 2 Comments

    Now an "administrative" post... no code here but a clarification about some support boundaries, since sometimes it happens that as Microsoft Technical Support engineers we can't help developers and device end-users and here I'd like to try explaining when and why.

    Above all, if you're in the Mobile-business you should know that Microsoft provides device manufacturers (OEMs) with a tool called “Adaptation Kit for Windows Mobile” (a special edition of “Platform Builder”): this contains about 90% of source-code of the Operating Systems and drivers’ sample codes. OEMs use this tool to adapt their Operating System to their hardware. They physically BUILD the platform after possibly having developed their own DRIVERS based on the samples that Microsoft provides: they might or might not follow Microsoft recommendations. All the DLLs you see in a Windows Mobile-based device have been compiled by the OEM, after possibly modifying the code provided by Microsoft as pure samples. Finally, it's completely up to the OEMs to choose which Security configuration select for their platforms. Therefore:

    - when device end-users have a problem with a driver, Microsoft may well not be able to help, because we have no clue about how the OEM customized their platform

    - when a problem is specific to a device model or manufacturer, chances are that it's due to a customization that OEM did and therefore Microsoft may not be able to help

    - developing DRIVERs for Windows Mobile is technically achievable by anyone, but only OEMs are meant to do so and therefore they only can ask for support. Only the OEM or device manufacturer of a particular device is equipped to support device driver development on their platform, Microsoft does not have the knowledge of the device specific implementation necessary to be able to assist third parties developing device drivers which have a direct dependency on the custom In-ROM implementation of each different Windows Mobile device. It’s up to an OEM to give the opportunity to driver-developers to develop drivers for their platforms. Some OEMs prefer to not give this opportunity to avoid possible system instability. Some OEMs prefer to give it to have an “open and customizable” product. This is the reason why you should refer to OEM’s Technical Support in those cases.

    On top of that OEMs may also provide a DEBUG build of their image: a RETAIL device doesn't have the debug symbols or the KITL-enabled features to allow an ISV Application Developer to get details of a problem. These are the reasons why Microsoft cannot help an ISV Application Developer to write a driver. Finally, Windows Mobile products don't have a DDK, and the necessary documentation to write a driver is provided only to Windows Mobile OEMs: the SDK doesn't cover drivers.

    If your goal is to develop a driver, then the best way you may have is to partner with one or more licensed Windows Mobile OEMs, and ask them to sponsor you as a Windows Mobile In-ROM IHV (or ISV). The OEM would need to nominate you and Microsoft would then evaluate that nomination. If the nomination was accepted you would be eligible to receive the Windows Mobile OEM Adaptation Kit, so you'll have access to the same source and samples that the OEMs have access to.

     

    Let's take another example: WiFi Programming. It’s up to the OEM to implement the WLAN stack (basically, the WiFi driver) by following or not the recommendations about WZC (Wireless Zero Configuration) APIs that Microsoft gives, and furthermore not every OEM allows developers to programmatically access the driver through a Private SDK. So, you'll have devices where you can even programmatically manage certificates associated to the WiFi connections, others where you can simply turn on\off the WiFi and others where you can't do anything... A Private SDK is a set of libraries that OEMs may create independently on the “standard” Windows Mobile’s SDK, available on any Windows Mobile-based device.

    Even if the WLAN driver the OEM developed does implement the WZC interfaces, the WZC APIs are usually a feature that only OEMs use, in order to eventually customize their WLAN-settings applications. Technically it's possible for ISV Application Developers to use WZC APIs (only on those devices for which the OEMs decided to implement the WZC Standard interfaces, in any case), but it’s not "supported", i.e. you can't ask for support in case you need help.

    An example about this was provided by OpenNETCF's guru Chris Tacke through the MSDN article Building a Wi-Fi Discovery Application with the .NET Compact Framework 2.0, however read: "[…] If a wireless card's driver is written to interface with WZC, it can be controlled and queried through this standardized interface, making configuration and status querying code consistent-regardless of the manufacturer of the adapter. While not all wireless cards are WZC-compatible, many are, and the high adoption rate is making it a defacto standard on both Windows-based desktop computers and Windows Mobile operating systems.".

  • Mobile Development - 'Support Side Story'

    Programmatically retrieve the GPRS’ APN configured for a Mobile Operator (XPath on Windows Mobile)

    • 0 Comments

    This was a good exercise as it showed how powerful XPath queries are, this time for XML Provisioning... Basically what the developer was interested on was the GPRSInfoAccessPointName of the CM_GPRSEntries Configuration Service Provider. The XML Provisioning Query was:

    <wap-provisioningdoc>
        <characteristic-query type="CM_GPRSEntries" />
    </wap-provisioningdoc>
    Once you have the desired query-xml, you can programmatically run it from within a NETCF v2.0 application by using the ConfigurationManager.ProcessConfiguration method (ConfigurationManager is a class of the Microsoft.WindowsMobile.Configuration namespace, defined in the Microsoft.WindowsMobile.Configuration.dll assembly).

    The code is:

    XmlDocument configDoc = new XmlDocument();
    configDoc.LoadXml(
        "<wap-provisioningdoc>" +
        "<characteristic-query type=\"CM_GPRSEntries\" />" +
        "</wap-provisioningdoc>");
    XmlDocument output = ConfigurationManager.ProcessConfiguration(configDoc, true);
    Now that you have a XmlDocument, you can use a XPath string to search the value you want: you can look at the resulting xmloutput of the RapiConfig in order to find the correct XPath string, which in this particular case is:
    /wap-provisioningdoc/characteristic/characteristic[@type="<INSERT GPRS ENTRY HERE>"]/characteristic/parm[@name="GPRSInfoAccessPointName"]

    I recommend using RapiConfig precisely in order to verify what’s the GPRS ENTRY name is configured in your case, otherwise the application may not work. In my testings I tried with a GPRS operator of mine (“TIM”). In NETCF, you can run a XPath query by using the .SelectSingleNode() or .SelectNodes() methods of the class XmlDocument:

    XmlNode aux = output.SelectSingleNode(
        "/wap-provisioningdoc/characteristic/characteristic[@type=\"" + 
        OperatorSettingToBeSearched + 
        "\"]/characteristic/parm[@name=\"GPRSInfoAccessPointName\"]");
     

    In conclusion, the sample code you may customize is (this was a NETCF v2.0 Console application):

    static string OperatorSettingToBeSearched = "TIM";
    static void Main(string[] args)
    {
        XmlDocument configDoc = new XmlDocument();
        configDoc.LoadXml(
            "<wap-provisioningdoc>" +
                "<characteristic-query type=\"CM_GPRSEntries\" />" +
            "</wap-provisioningdoc>");
        XmlDocument output = ConfigurationManager.ProcessConfiguration(configDoc, true);
        XmlNode aux = output.SelectSingleNode("/wap-provisioningdoc/characteristic/characteristic[@type=\"" +
            OperatorSettingToBeSearched + 
            "\"]/characteristic/parm[@name=\"GPRSInfoAccessPointName\"]");
        MessageBox.Show(aux.Attributes["value"].Value);
    }
  • Mobile Development - 'Support Side Story'

    Establishing GPRS Connection on Windows CE and Windows Mobile: Sample Codes

    • 22 Comments

    Sometimes developers asking for support have straightforward goals they want to achieve with the minimal amount of code, even if the "application" will work only on very specific scenarios. This was the case for a request I handled, where the goal was: connect the device via GPRS to Internet, when it's not cradled to a desktop PC and WiFi is not available. As you may know connections in Windows Mobile are entirely handled by the Connection Manager, and developers are "encouraged" on using only Connection Manager APIs, as long as you want CM-aware applications to run as expected. For example, IE Mobile is a CM-aware application: if you open a GPRS connection through RAS APIs as documented in the Windows *CE* documentation (see here), then Connection Manager wouldn't sense it and IE Mobile would request a new one. A well done article about this for mobile developers was written by Jim Wilson and is available here.

    So, the "recommended" way on Windows Mobile is to invoke CM APIs, while on Windows CE you can use for example RAS APIs (CM is not available) - see below. Note that this code is meant to work when no other less expensive networks are available (ActiveSync\WMDC's Desktop-Passthrough, WiFi).

    #include "stdafx.h"
    #include <windows.h>
    #include <commctrl.h>
    #include "connmgr.h"
    
    
    
    GUID GetNetworkForURL (LPCTSTR url)
    {
        DWORD dwIndex = 0;
        GUID rv;
        if (!SUCCEEDED (ConnMgrMapURL (url, &rv, &dwIndex)))
            rv = GUID_NULL;
    
        return rv;
    }
    
    
    HRESULT EstablishConnection (LPCTSTR url, DWORD retries, DWORD timeout, DWORD* dwStatus, HANDLE hConnection)
    {
        CONNMGR_CONNECTIONINFO ConnectionInfo;
        ZeroMemory (&ConnectionInfo, sizeof (ConnectionInfo));
        ConnectionInfo.cbSize = sizeof (ConnectionInfo);
    
        ConnectionInfo.dwParams = CONNMGR_PARAM_GUIDDESTNET; 
        ConnectionInfo.dwFlags = CONNMGR_FLAG_PROXY_HTTP; 
        ConnectionInfo.dwPriority = CONNMGR_PRIORITY_USERINTERACTIVE; 
        ConnectionInfo.guidDestNet = GetNetworkForURL (url);
    
        return ConnMgrEstablishConnectionSync (&ConnectionInfo, &hConnection, timeout, dwStatus);
    }
    
    
        //
        // Called when there was an error while connecting
        // generally due to network connection not being available (no modem, no nic etc).
        //
        HRESULT DoConnectingError(DWORD dwStatus)
        {
            // we received an error to do with connecting.
            SHELLEXECUTEINFO sei = {0};
            TCHAR szExec[MAX_PATH];
            wsprintf( szExec, TEXT("-CMERROR 0x%x -report"), dwStatus );
            sei.cbSize = sizeof(sei);
            sei.hwnd = NULL;
            sei.lpFile = TEXT(":MSREMNET");
            sei.lpParameters = szExec;
            sei.nShow = SW_SHOWNORMAL;
            ShellExecuteEx( &sei );
            return E_FAIL;
        }
    
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        DWORD dwStatus = 0;
        HANDLE hConnection = NULL;
    
        LPCTSTR url = L"http://www.msn.com"; //just to set Internet network - and no need for proxies
        HRESULT hr = EstablishConnection (url, 10, 25000, &dwStatus, hConnection);
    
        if ( FAILED( hr ) )
        {
            DoConnectingError(dwStatus);
        }
    
        ConnMgrReleaseConnection(hConnection, TRUE);
        return 0;
    }

     

    while on Windows CE-based devices:

    //TODO: ADD ERROR-CHECKING
    BOOL ConnectGPRSonWindowsCE()
    {
        HRASCONN hRASConnection = NULL;
        DWORD nRet;
    
        RASENTRYNAME TempEntryName;
        TempEntryName.dwSize = sizeof(RASENTRYNAME);
        unsigned long EntryBufferSize = sizeof(TempEntryName);
        unsigned long EntryWritten = 0;
    
        //ERROR_BUFFER_TOO_SMALL is expected
        nRet = RasEnumEntries( NULL, NULL, &TempEntryName, &EntryBufferSize, &EntryWritten );
        if (nRet != ERROR_BUFFER_TOO_SMALL)
        {
            WCHAR strErr[255];
            wsprintf(strErr,L"RasEnumEntries failed: Error %d\n", nRet);
            MessageBox(NULL, (LPCTSTR)strErr, L"GPRS Monitor", MB_OK);
            return FALSE;
        }
    
        RASENTRYNAME *RasEntryNameArray = (RASENTRYNAME*)malloc(EntryBufferSize);
        RasEntryNameArray[0].dwSize = sizeof(RASENTRYNAME);
        nRet = RasEnumEntries( NULL, NULL, RasEntryNameArray, &EntryBufferSize, &EntryWritten );
        if (0 != nRet)
        {
            WCHAR strErr[255];
            wsprintf(strErr,L"RasEnumEntries failed: Error %d\n", nRet);
            MessageBox(NULL, (LPCTSTR)strErr, L"GPRS Monitor", MB_OK);
            return FALSE;
        }
    
        int GPRSEntry = -1;
    
        RASENTRY RasEntry;
        memset( &RasEntry, 0, sizeof(RasEntry) );
        RasEntry.dwSize = sizeof(RasEntry);
        DWORD dwEntrySize = sizeof(RasEntry);
        unsigned char Buffer[4098];
        memset( Buffer, 0, sizeof(Buffer) );
        DWORD dwBufferSize = sizeof(Buffer);
    
        for ( unsigned long iEntry = 0; iEntry < EntryWritten; iEntry++ )
        {
            // Check if the name has GPRS in it 
            // AND
            // if Local Phone Number contains "~GPRS!"
            nRet = RasGetEntryProperties(NULL, RasEntryNameArray[iEntry].szEntryName, &RasEntry,&dwEntrySize,NULL,NULL);
            if (0 != nRet)
            {
                WCHAR strErr[255];
                wsprintf(strErr,L"RasGetEntryProperties failed: Error %d\n", nRet);
                MessageBox(NULL, (LPCTSTR)strErr, L"GPRS Monitor", MB_OK);
                return FALSE;
            }
    
            if ((wcsstr(RasEntry.szLocalPhoneNumber,L"~GPRS!") != NULL) 
                &&
                (wcsstr(RasEntryNameArray[iEntry].szEntryName, L"GPRS") != NULL))
            {    
                //RAS entry is GPRS - exit 'for' loop
                GPRSEntry = iEntry;
                break;
            }
        }
    
        nRet = RasGetEntryProperties( NULL, RasEntryNameArray[GPRSEntry].szEntryName, &RasEntry, &dwEntrySize, Buffer, &dwBufferSize );
        if(0 != nRet)
        {
            WCHAR strErr[255];
            wsprintf(strErr,L"RasGetEntryProperties failed: Error %d\n", nRet);
            MessageBox(NULL, (LPCTSTR)strErr, L"GPRS Monitor", MB_OK);
            return FALSE;
        }
    
        // Configure the RASDIALPARAMS structure
        RASDIALPARAMS RASDialParameters;
        memset( &RASDialParameters,0,sizeof( RASDIALPARAMS ) );
        RASDialParameters.szPhoneNumber[0] = NULL; //TEXT('\0');
        RASDialParameters.szCallbackNumber[0] = NULL; //TEXT('\0');
        RASDialParameters.dwSize = sizeof( RASDIALPARAMS );
        wcscpy( RASDialParameters.szEntryName, RasEntryNameArray[GPRSEntry].szEntryName);
        RASDialParameters.szUserName[0] = TEXT('\0');
        RASDialParameters.szPassword[0] = TEXT('\0');
        RASDialParameters.szDomain[0] = TEXT('\0');
        //wcscpy (RasDialParams.szUserName, szUserName); //This is optional    
        //wcscpy (RasDialParams.szPassword, szPassword); //This is optional
        //wcscpy (RasDialParams.szDomain, szDomain); //This is optional
    
    
        //try reuse GPRS connection
        BOOL bPassword = FALSE;
        nRet = RasGetEntryDialParams(NULL, &RASDialParameters, &bPassword);
        if (0 != nRet)
        {
            WCHAR strErr[255];
            wsprintf(strErr,L"RasGetEntryDialParams failed: Error %d\n", nRet);
            MessageBox(NULL, (LPCTSTR)strErr, L"GPRS Monitor", MB_OK);
            return FALSE;
        }
    
        //free resources not on the stack
        free ((VOID*)RasEntryNameArray);
        
                    
        //  Try to establish RAS connection.
        if ( RasDial( NULL, NULL, &RASDialParameters, 
                NULL, // Notifier type is NOT a window handle
                NULL, // Window receives notification message - none
                &hRASConnection ) != 0 )
        {
            if ( hRASConnection != NULL )
                RasHangUp( hRASConnection );
            hRASConnection = NULL;
            MessageBox (/*hDlgWnd*/NULL, L"Could not connect using RAS", /*szTitle*/ L"GPRS Monitor", MB_OK);
            return FALSE;
        }
    
        hRASConnection = NULL;
        return TRUE;
    
    }
     
  • Mobile Development - 'Support Side Story'

    Subclassing NETCF Applications

    • 9 Comments

    Some time ago I handled a request coming from a developer that wanted to intercept Windows Messages sent to another application, in other terms "subclass" the other application's main window. On Desktop's Win32 you can do that by setting a hook through SetWindowsHookEx (which exists on Windows CE but it's a private API), but due to the specific nature of the Win32 implementation under Windows CE before release 6.0, a process can access the memory space of another one: this has been done in the past due to the very tight limitations about Virtual Memory imposed on a Windows CE platform (32 processes, each of them 32MB of virtual space). You might read this great article written by Doug Boling some years ago, but still valid for Windows Mobile as long as it's based on platforms before Windows Embedded CE 6.0 (the version of the same article for Windows CE 6.0 is here).

    Therefore on Windows Mobile 5.0 and 6 (both based on Windows CE 5.0 despite of the name! Smile) GetWindowLong() API with GWL_WNDPROC flag works even if you call it for a different process (contrarily to a real Win32-based OS, like desktops and Windows CE 6.0). This allows us to intercept messages in this way, in a managed application:

    IntPtr hwnd = IntPtr.Zero;
    hwnd = FindWindow(strClassName, strWindowName);
    newWndProc = new WndProcDelegate (NewWndProc);
    oldWndProc = GetWindowLong(hwnd, GWL_WNDPROC);
    int success = SetWindowLong(hwnd, GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(newWndProc));
    being for example, in case you want to intercept WM_ACTIVATE:
    public IntPtr NewWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
    {
        if (msg == WM_ACTIVATE)
        {
            MessageBox.Show("hook!");
        }
        return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam);
    }
     
    This approach is based on the assumption that your application knows target's window's Class Name and Window Name, but this might not be always true! Moreover, it works with NATIVE applications, because native applications have a predefined and set ClassName and WindowName, so FindWindow() API can easily return the window handle of the process you want to subclass. This is not true with NETCF applications: the 1st managed launched application has "#NETCF_AGL_BASE_" as ClassName, the second one an empty string; there are some other patterns as well, and they are UNDOCUMENTED, thus meaning that they can change for future releases, precisely as they did from v1 to v2 (see Daniel Moth's post about this).

    So, in case you want to subclass NETCF applications and you only know the executable's name, a possible approach is to enumerate active processes to get the ID of the one you’re interested on, so that you can use System.Diagnostic.Process.MainWindowHandle property to retrieve the window handle you want to subclass. Unfortunately NETCF's System.Diagnostic.Process class doesn’t implement all the methods that would have helped, for example .GetProcesses(): however, you can mix up a “Process” class exposing a .GetProcesses() method with the System.Diagnostic.Process to exploit its .MainWindowHandle. Such custom class is provided as a sample by the following MSDN article: Creating a Microsoft .NET Compact Framework-based Process Manager Application (and I imagine it was the internal implementation of SDF v1.4's OpenNetcf.Diagnostics.Process).

     

    The result is the sample code below, as usual provided "AS IS" (this is not "production-code", it's meant to have didactic\testing purposes):

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    using System.Reflection;
    using System.IO;
    using System.Diagnostics;
    
    namespace SubclassDemo
    {
        public partial class Form1 : Form
        {
            private static IntPtr oldWndProc = IntPtr.Zero;
            private static WndProcDelegate newWndProc;
            private static IntPtr hWndTarget = IntPtr.Zero;
            private string strClassName = string.Empty;
            private string strWindowName = string.Empty;
    
            public Form1()
            {
                InitializeComponent();
    
                ////txtTargetExeName is a TextBox where user enters target application's name
                //LaunchSecondApp(txtTargetExeName.Text);
    
                //make sure that our application has the focus
                SetForegroundWindow(this.Handle);
            }
    
            ////assumption: target application is under the same path of running application
            //private void LaunchSecondApp(string strExeName)
            //{
            //    string strAppPath = Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;
            //    strAppPath = Path.GetDirectoryName(strAppPath);
            //    strAppPath = strAppPath.Trim();
            //    if (!strAppPath.EndsWith("\\")) 
            //        strAppPath += "\\";
            //    Process.Start(strAppPath + strExeName, string.Empty);
            //}
    
            // SET if you know strClassName and strWindowName of the window you want to subclass
            private void btnSetHookKnowingNames_Click(object sender, EventArgs e)
            {
                hWndTarget = FindWindow(strClassName, strWindowName);
                newWndProc = new WndProcDelegate(NewWndProc);
                oldWndProc = GetWindowLong(hWndTarget, GWL_WNDPROC);
                int success = SetWindowLong(hWndTarget, GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(newWndProc));
            }
    
            //RESET (before closing) if you know strClassName and strWindowName of the window you want to subclass
            private void btnRemoveHookKnowingNames_Click(object sender, EventArgs e)
            {
                int success = SetWindowLong(hWndTarget, GWL_WNDPROC, oldWndProc);
            }
            
            //SUBCLASSING PROCEDURE
            public IntPtr NewWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
            {
                //defined WM_RAFFAEL for testing purposes
                if (msg == WM_RAFFAEL)
                {
                    MessageBox.Show("hook!");
                    return IntPtr.Zero;
                }
                return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam);
            }
    
            //TEST
            private void btnTest_Click(object sender, EventArgs e)
            {
                SendMessage(hWndTarget, WM_RAFFAEL, 0, 0);
            }
    
            //private string strTargetAppExeName = "TestMANAGED".ToLower();
    
            //SET if you don't know strClassName and strWindowName: this is true for managed applications
            private void btnManagedSet_Click(object sender, EventArgs e)
            {
                //You know the process's executable name (and path, if required)
                //of the 2nd managed application. --> strTargetAppExeName
    
                ////Now:
                ////Enumerate all the processes, in order to get their ProcessID
                ////For each ProcessID, check if its executable is the one we're interested on
                ////Stop when we find the right Process ID
                ////For example something similar to:
                //IntPtr pID = GetProcessIDOfTheManagedApp(strTargetAppExeName);
    
                ////Enumerate all the windows and for each window use
                ////GetWindowThreadProcessID to see if it's associated to the
                ////ProcessID we found
                ////stop when finding a window associated to that process
                ////then use GetWindowLong(PARENT) to verify it's the main window of that process
                ////For example something similar to:
                //hWndTarget = GetMainWindowOfTheManagedApp(pID);
    
                ////BUT...
                //Utilities.Process class exposes .GetProcesses()
                //System.Diagnostic.Process exposes .MainWindowHandle
    
                Utilities.Process[] processes = Utilities.Process.GetProcesses();
                foreach (Utilities.Process p in processes)
                {
                    //string name = p.ToString().ToLower();
                    //strTargetAppExeName = strTargetAppExeName.ToLower();
                    //if (p.ToString().ToLower() == strTargetAppExeName)
    
                    //since we're comparing 2 strings, both potentially manually entered by user
                    //use .ToLower() to modify the capital letters
                    //use .TrimEnd(".exe".ToCharArray() to remove, if any, the ".exe" from the application name
                    //this is also because in some cases managed applications are reported as without .exe at the end in the name
                    if (p.ToString().ToLower().TrimEnd(".exe".ToCharArray()) == txtTargetExeName.Text.ToLower().TrimEnd(".exe".ToCharArray()))
                    {
                        System.Diagnostics.Process pp = System.Diagnostics.Process.GetProcessById((int)p.Handle); //Handle and PID are the same in WINCE
                        hWndTarget = pp.MainWindowHandle;
                    }
                }
                
                //as per native apps, associate new WndProc
                newWndProc = new WndProcDelegate(NewWndProc);
                oldWndProc = GetWindowLong(hWndTarget, GWL_WNDPROC);
                int success = SetWindowLong(hWndTarget, GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(newWndProc));
            }
    
    
            //private void GetProcessIDOfTheManagedApp(string strTargetAppExeName)
            //{
            //}
    
            //private void GetMainWindowOfTheManagedApp(IntPtr processID)
            //{
            //}
    
            //REMOVE the hook if you're closing!
            private void Form1_Closing(object sender, CancelEventArgs e)
            {
                int success = SetWindowLong(hWndTarget, GWL_WNDPROC, oldWndProc);
            }
    
            #region DllImport
            public const int GWL_WNDPROC = (-4);
            public const int WM_RAFFAEL = 123456789;
    
            public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
    
            [DllImport("coredll", SetLastError = true)]
            public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
    
            [DllImport("coredll", SetLastError = true)]
            public static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr newWndProc);
    
            [DllImport("coredll", SetLastError = true)]
            public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    
            [DllImport("coredll.dll", SetLastError = true)]
            private static extern IntPtr FindWindow(string _ClassName, string _WindowName);
    
            [DllImport("coredll.dll", SetLastError = true)]
            public static extern int SendMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);
    
            [DllImport("coredll.dll", SetLastError = true)]
            private static extern bool SetForegroundWindow(IntPtr hWnd);
            #endregion DllImport
        }
    }

     

  • Mobile Development - 'Support Side Story'

    HOPPER: Start Menu dead, not responding

    • 0 Comments

    Hopper is an extremely powerful tool OEMs have always had to test the stability of their PLATFORMs. With WM6 SDK, Hopper has been provided to ISV Application Developers as well, with a customizable FocusApp sample that maintains an application (or a group of) at foreground while tests are running. Refer to the "official" blog for whatever detail.

    Some time ago I handled a request about a strange error with Hopper: I'm writing here as I noticed that some other developers had the same issue but this seems to be an unresolved topic (so this is going to be an "informative non-coding post"..). Hopper was failing its tests with the message "Start menu dead, not responding", even if after closing Hopper the tester could see that the application and the system were behaving as expected. After some investigation (thanks also to Reed&Steve, whose blog is here), we figured out that if the application is full-screen (i.e. it invokes SHFullScreen() API) then the programmer must add a message handler for VK_RETURN or VK_ESCAPE.

    Hopper switches the application that should be tested after a while: this is its design so that it tests whole applications. To switch application, hopper uses start menu by sending VK_LWIN or clicking left upper side. In case it doesn’t response at all or hopper can’t detect start menu then it dismisses current foreground windows and try it again. But in order to dismiss the foreground window, this must handle VK_RETURN or VK_ESCAPE.

    P.S: How to attach a debugger and use FocusApp so that you can have your application under the VS debugger while the test runs? Check out this.

  • Mobile Development - 'Support Side Story'

    MSMQ on Windows Mobile: Troubleshooting Guide

    • 2 Comments

    I've handled in the past many requests about installation and configuration issues around MSMQ on Windows Mobile, therefore here it is the "status of the art" of my personal troubleshooting guide...

    Above all, check if you’re installing the right CAB. MSMQ for WM5 is different from the one contained in the WM2003 SDK, and it’s not contained in the WM5 SDK. It’s now part of the Redistributable Server Components for Windows Mobile 5.0.

    Then, you need to either (a) install the SdkCerts.cab on the test device or (b) sign the msmq.arm.cab with a certificate you know is installed on the device. For SOME smartphones (depending on OEM’s choice about security), contrarily to Pocket PCs, you might have to sign internal executables (EXE, DLL) as well, for example through the utility cabsigntool.exe. This is required on Windows Mobile 5.0- and 6-based devices since MSMQ needs to start as a priviledged service, therefore it must be signed with a certificate stored in the Priviledge Certificate Store, due to the Security-related features of the OS. For testing purposes only, you can use the SDK Test Certificates (SdkCerts.cab), considering that the MSMQ’s CAB coming with the download above (msmq.arm.CAB) is actually signed with that certificate: then, if you’ll need to sign the CAB with another certificate, you can simply add a new certificate to the chain of the CAB, because Windows Mobile will look only at the last certificate signing a file.

    In any case if you're running in troubles with MSMQ installation\configuration I usually suggest starting with the SDK certificates just to understand if the problem you’re experiencing is related to security (see “note about security” below).

    The steps for correctly installing MSMQ on WM5 include a soft-reset. On some devices, it happened that because of a problem of the OEM’s customized WM5-based image, modifications to the registry were not persisted during a soft-reset. A temporary solution for this (waiting for the next ROM Upgrade from OEMs) was to invoke the RegFlushKey() API from an application:

    RegFlushKey(0x80000002) //“0x80000002" simply means HKEY_LOCAL_MACHINE

    Sincerely it's about time that I don't encounter a device that doesn't flush registry modifications when soft-resetting... and in any case before doing this you could simply use a Registry viewer to see if after a soft-reset the modifications are persisted on your specific device.

    Finally, as you may know an application that needs to use MSMQ has to activate this service before calling it, through the call

    ActivateDevice(@"Drivers\BuiltIn\MSMQD", 0);

    – this has been documented since WM2003 days through this blog from the NETCF Dev Team, which explained how to develop NETCF applications using MSMQ (at that point in time only WM2003 was available, so it was using the CAB for WM2003).

    Keep in mind that you may need to change AllowCmdLine registry to 1 as described by Services.exe Registry Settings.

    If you’re interested on using MSMQ within a NETCF application, you can use the same code sample of the blog as a template for MSMQ on WM5, paying attention on modifying the constant strings. Or, you could consider the suggestions above and modify the following code, provided “AS IS”:

     

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    namespace MSMQ_patch
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
            private void button1_Click(object sender, EventArgs e)
            {
                try
                {
                    long reg = RegFlushKey(0x80000002);
                    if (reg == 0)
                        MessageBox.Show("Registry saved");
                    else
                        MessageBox.Show("Error: " + System.Runtime.InteropServices.Marshal.GetLastWin32Error().ToString());
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error: " + ex.Message);
                }
            }
            private void button2_Click(object sender, EventArgs e)
            {
                try
                {
                    IntPtr handle = ActivateDevice(MSMQ_DRIVER_REG, 0);
                    if (handle != null)
                    {
                        MessageBox.Show("Activated");
                        CloseHandle(handle);
                    }
                    else
                        MessageBox.Show("Error: " + System.Runtime.InteropServices.Marshal.GetLastWin32Error().ToString());
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error: " + ex.Message);
                }
            }
            [System.Runtime.InteropServices.DllImport("CoreDll.dll", EntryPoint = "RegFlushKey", SetLastError = true)]
            private static extern uint RegFlushKey(uint hKey);
            private const String MSMQ_DRIVER_REG = @"Drivers\BuiltIn\MSMQD";
            [System.Runtime.InteropServices.DllImport("CoreDll.dll", SetLastError = true)]
            private static extern IntPtr ActivateDevice(string lpszDevKey, Int32 dwClientInfo);
            [System.Runtime.InteropServices.DllImport("CoreDll.dll", SetLastError = true)]
            private extern static Int32 CloseHandle(IntPtr hProcess);
        }
    }

    After checking that registry is flushed correctly after a soft-reset, you might also convert the application into a Console Application that invokes ActivateDevice() and then launch visadm.exe – I’m talking about something similar to:

    static void Main(string[] args)
    {
        try
        {
            IntPtr handle = ActivateDevice(MSMQ_DRIVER_REG, 0);
            //if (handle != null)
            //{
            //    MessageBox.Show("Activated");
            //    CloseHandle(handle);
            //    return;
            //}
            //else
            //{
            //    MessageBox.Show("Error: " +
            //        System.Runtime.InteropServices.Marshal.GetLastWin32Error().ToString());
            //}
            
            Process.Start(@"\Windows\visadm.exe","");
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error: " + ex.Message);
        }
    }
        
    private const String MSMQ_DRIVER_REG = @"Drivers\BuiltIn\MSMQD";
    [System.Runtime.InteropServices.DllImport("CoreDll.dll", SetLastError = true)]
    private static extern IntPtr ActivateDevice(string lpszDevKey, Int32 dwClientInfo);
    [System.Runtime.InteropServices.DllImport("CoreDll.dll", SetLastError = true)]
    private extern static Int32 CloseHandle(IntPtr hProcess);

    You can also use that application as it is, purely to perform the following TEST:

    (0. hard-reset the test device)

    1. install SdkCerts.cab on the device

    2. install msmq.arm.cab on the device

    3. run \Windows\visadm.exe

    4. tap on Shortcuts, then “Install” and finally “Register”

    5. launch the app above and click on button1 (to invoke RegFlushKey)

    6. soft-reset the device

    7. launch again the app above and now click on button2 (to invoke ActivateDevice)

    8. run \Windows\visadm.exe

    9. tap on Shortcuts, then “Verify”: it should that MSMQ and NETREG daemons are installed

    10. now tap on Shortcuts, then “Status”: it should return “DISCONNECTED”, which is the expected result at this point

    NOTE ABOUT SECURITY:

    The download “Windows Mobile 5.0 Server Components” is meant to be used by ISV Application Developers and therefore during the developing\testing phase of the application it simply needs the SDK Certificates on the test target devices. However, when deploying the application, since the SDK Certificates cannot be “redistributed” along with an application, an ISV Developer needs to sign the CAB *AND* its binaries inside (for example by using cabsigntool.exe) with a certificate that he\she knows will be registered in the Priviledge Certificate Store (this is for the binaries) and the SPC Store (this is for the CAB) of the target device. This is because the MSMQ service is registered during system boot. This heavily depends on the Security Configuration chosen by the device’s OEM.

    You can use your own certificate, however you must be sure that the target device allows you to install that new certificate you’ll use (OEMs can also choose if end-users can install new certificates). Usually, ISV Developers ask to device OEMs or Mobile Operator which certificates they can use to sign their application: this is valid for the MSMQ cab and its binaries inside as well. As an ISV Application Developer, possibly you already own a Mobile2Market certificate you use to sign your applications: you might use the same M2M certificate for the MSMQ CAB and its files. Some other info about how ISV Application Developers should approach to Windows Mobile 5.0 Security are available at Windows Mobile 5.0 Application Security.

  • Mobile Development - 'Support Side Story'

    NETCF: Memory leak... now what??

    • 10 Comments

    Subtitles:

    - OutOfMemoryException (OOM)

    - SqlCeException: Not enough memory to complete this operation

    - ...

     

    I'm a Support Engineer at Microsoft CSS ("Customer Service and Support"), and I pretty often handle requests related to NETCF applications crashing with OOM messages, or under a heavy memory pressure and therefore performing bad. I collected some hints and techniques ISV Application Developers may use to prevent or troubleshoot it, so that I can point next developers to them by using a link and not copying\pasting every time the same initial mail... :-) [which frankly btw addresses half of the requests related to leaks...] Happy reading!

     

    GENERIC SUGGESTIONS

    It’s quite difficult to get a comprehensive set of actions in order to debug a memory leak, as it’s dependent on the specific scenario you’re running. So, some of the following suggestion could be useful in your case, some others couldn’t. Accordingly to Three Common Causes of Memory Leaks in Managed Applications the 3 most common causes are:

    1. unmanaged resources not correctly cleaned up
    2. drawing resources not correctly .Dispose-d
    3. still referenced managed objects

    Regarding (1), if you're developing custom controls then remember to implement Finalize & Dispose, as recommended by Implementing Finalize and Dispose to Clean Up Unmanaged Resources.

    Regarding (2), consider the following subtle things you should know: drawing NETCF classes such as Font, Image, Bitmap, Pen, Brush, etc are tiny wrappers around the native resources, which are handled in Windows CE by the GWES (Graphics, Windowing and Event Subsystem). This simply means that in NETCF applications, when you instantiate such classes you must invoke .Dispose on the object, otherwise a leak will be produced. This is the same for MODAL forms (myForm.ShowDialog) as I pointed above: call .Dispose() from the method showing the dialog.

    Note that, because of a bug addressed in v3.5, calling Dispose() on an ImageList object causes a memory leak. Therefore, if you’re using an ImageList, do not call Dispose() on it, unless you're application is targeting 3.5. In any case you must clear the list if you’ve finished with it:

    frm.ImageList1.Images.Clear();
    
    // NOT NEEDED frm.ImageList1.Images[0].Dispose();
    // NOT NEEDED frm.ImageList1.Images[].Dispose();
    // DO NOT frm.ImageList1.Dispose();
    // because it leaks in NETCF v2 – addressed in v3.5

    The same applies to Toolbars, because they have a .ImageList property. When you close the form and free up the images, don’t call .Dispose on the .ImageList property of the toolbar, but simply

    frm.ToolBar1.ImageList.Images.Clear()

     

    Regarding (3), you can leverage on some monitoring-tools (below) and you should be aware of some caveats, some of which are described in How to: Improve Performance (.NET Framework Developer's Guide). So for example:

    • Since String objects are immutable in .NET, every time you modify the value of a String internally a new String object is created. Use StringBuilder instead of concatenating (“+”) String objects, because otherwise too much garbage will be created and the GC will kick too often, thus affecting the whole performances of the application.
    • Avoid "NEW" statements in loops
    • Limit the number of open SqlCeCommand objects and dispose of them when finished
    • ...

    TAKE SOME TIME to read through that article...

    Still about (3), remember that if you add an event handler to a control that has been dynamically created, before disposing the control you need to explicitly remove that eventhandler - otherwise a reference will be hold for that control and it won’t be effectively garbage-collected.

     

    SPECIFICALLY ABOUT SQLCE\SQL Mobile\SSCE:

    Again, some generic suggestions from the experience I had with some cases.

    Do not use a shared\static SqlCeConnection object (or a class’ field) and Open\Close it as long as strictly required. While the SqlCeConnection is open, "hidden" native resources are allocated to access the database (query plans, temp data, etc). If opening the connection takes unacceptably long, then you need to find an appropriate balance with caching the connection.

    What is the benefit of closing a SqlCeConnection immediately after finished with it, in terms of process' Virtual Memory? The SSCE engine is not a service: when you open the first connection, you initialize the engine. At that time the engine consumes minimal resources. Once you start running queries, it acquires memory from the OS to process those queries, e.g. space for query plans, result set buffers, etc. The SSCE engine does not dispose of the buffers immediately: it would keep them aside for later use so that we don’t need to request the OS again and again. This is due to the internal heap management architecture. Closing the connection results on the engine disposing everything it used so far.

    Note that the size of NATIVE resources (in terms of virtual memory) associated to a SqlCeConnection depends mostly on the size of the database, not with the complexness of the query. Native resources associated to the connections the the database may take up to 2MB in size (rarely with more than this, even for really big databases). Finally, being NATIVE resources, they are not shown up in NETCF RPM counters.

    Reduce the “Max Buffer Size” (in the connection string, from its default value 640KB to for example 256KB): modifications are flushed to the memory more often, but native resources holding process’ virtual memory is less (virtual memory hold by "hidden" native resources). The balance is:

    • in order to reduce NATIVE MEMORY consumption, you want to decrease the "Max Buffer Size"
    • in order to increase application's perceived PERFORMANCES, you want to increase the "Max Buffer Size"

    Max Buffer Size sets the largest amount of memory, in KB, that SQLCE storage engine can use before it starts flushing changes to disk. Optimal values for Max Buffer Size depend on the data schema and the load. It is important to understand the impact of this parameters for optimal performance. An accurate approach would be to profile the performance and reach the optimal value. A suggested profiling approach would be:
    1. Choose the query which is costliest in amount of data that needs to be processed, query execution time spent and joining your bulkiest tables amongst your query set.
    2. Time query executions with Connection Strings exploring a range of Max Buffer Size values.

    Specifically if you work with multiple databases concurrently, reducing the the Max Buffer Size in all the connections can give a lot of virtual memory back to the process...

    Point the temp db to a storage card (from BOL: "If a temporary database location is not specified, then the current database location is used as the location of the temporary database"), to save storage and physical RAM. This is done in the connection string.

    If you set Mode=Read Only in the connection string for those databases that you know the code won’t modify, then you must specify a location for the temp db (otherwise, error 25120 - SSCE_M_RODATABASEREQUIRESTEMPPATH)

    Close or dispose all SSCE Objects when done (I think this is written everywhere on the web :-)

    Reduce usage of memory intensive data structures: datasets in primis... I know that programming with datasets is easy, especially together with DataAdapters, however they were not introduced for a scarce-memory scenario such as Windows Mobile (at least so far). And datasets are a local duplication of the same data you have on the database: thus, in general they can be seen as a data cache that enhances perceived performances. They were introduced to work in disconnected scenarios, such as web pages for example: but when an application accesses to a local SSCE database, then datasets may not be the right ADO.NET approach for the data layer. You may consider SqlCeResultSet instead, or leverage on the performance provided by DataReaders. More on this on a later post I presume...

    In any case, if you think that caching much data improves perceived performances of the application, remember that when many hundreds of KB of the Managed Heap are hold by data caches, then the GC will kick more often. And a GC is very expensive, as it needs to "freeze" all application's threads to run. And, as you'll see below, the Managed Heap is usually not larger than 1~3 MB in total for real-world applications.

    KB\Doc:

    NETCF AND VIRTUAL MEMORY:

    If you've never looked at how the memory looks like on Windows CE-based platforms, you may start from this 2-part blog post:

    Slaying the Virtual Memory Monster [DumpMem explained] - 1st part

    Slaying the Virtual Memory Monster [DumpMem explained] - 2nd part

    I won't enter on the details described in that blog, so let me write down some probably useful notes:

    1- The DLLs related to the NETCF loaded in the application's address space (aka "process slot") are only the ones for the CLR (netcfagl* and mscoree*, in case they’re not XIP DLLs already in ROM, thus meaning they're loaded at slot 1): all the other managed assemblies are loaded into the Large Memory Area (1GB\2GB in the virtual address space). Therefore, the only impact managed modules have in the application’s address space is the JITed code. This is why "unloading assembly", even if it was possible, doesn't give any benefit. This is documented by Device Memory Management (.NET Framework Developer's Guide) and by Mike Zintel’s blog (he was the Program Manager for NETCF Product Group) .Net Compact Framework Advanced Memory Management

    2- The code+stack+heap can be allocated ABOVE the “DLL Load Point”: this is NOT a problem. They can oversee that line and can even fill blank spaces between separate loaded DLLs. An OOM may happen if:

    * a NEW library is loaded (i.e. a library that wasn’t loaded by any other process so far, thus meaning that the DLL won't find any available space downwards)

    * there’s no more room for loading code+stack+heap even in the blank spaces among the loaded DLLs (virtual memory is completely full, considering also fragmentation)

    3- COSTs:

    only mscoree*_0.dll and netcfagl*_0.dll are loaded into a managed application’s virtual memory address space (in case they’re not XIP DLLs already in ROM), and this is the only fixed cost of a managed application (roughly 750KB). Dynamic costs might be:

    * CLR Data structures: ~250KB

    * JIT Heap: ~500KB (remember about “Code Pitching”, i.e. the ability to throw away the JIT-ed code in case of scarce memory)

    * for each thread: a minimum of 64KB-stack (and a NON-multithreaded managed application has 2 or 3 threads)

    * GC Heap: ~2MB, even if a Garbage Collection takes place every 1MB of managed objects allocated (moreover, note that when allocating an object, a VirtualAlloc is invoked for at least a 64KB-segment

    * there are other heaps (AppDomain, Process, Short-Term), in any case what RPM calls “Total size of objects” is the total of all of them.

    Apart from the cost associated to “pure” managed memory, don’t forget NATIVE resources, for example the ones associated to:

    - SSCE engine

    - Graphical resources

    - P/Invoke or COM Interop

    - Fragmentation when loading a DLL:

    WM5: 64KB-segments

    WM6: 4KB-segments

    This means even if you loaded two DLLs that were 10K in size, they would eat up 128K of VM on WM5.  In WM6, we use a 4K boundary which makes more efficient use of memory and those two DLLs would eat up 24KB in total.

    - Usual values:

    * Mscoree2_0.dll + netcfagl2_0.dll (NETCF native code)

                Virtual:  650KB

                Physical:            475KB (assume 75% hot)           

                VM [32MB] or Large Files Area [1GB]?     VM

    * NETCF Memory Mapped Files (system.dll, mscorlib.dll, etc)        

                Virtual: 3.8MB (worst case)        

                Physical:            1MB (assume 25% hot)  

                VM [32MB] or Large Files Area [1GB]?     1GB space (worst case)

    * Other Assemblies, including the managed application’s EXE       

                Virtual: File size x 150% (uncompressed)

                Physical:            Assume file size x 25%  

                VM [32MB] or Large Files Area [1GB]?     1GB space (worst case)

    * CLR Data Structures    

                Virtual: 250KB  

                Physical:            250KB  

                VM [32MB] or Large Files Area [1GB]?     VM

    * JIT Heap [this can be “pitched” under low-memory]       

                Virtual:  500KB  

                Physical:            500KB  

                VM [32MB] or Large Files Area [1GB]?     VM

    * GC Heap         Max (1MB, reachable objects’ set (= GC.GetTotalMemory))

    [it can reach 2 / 3 MB]

    [for each allocated object, at least 64KB --> “segment” = 64KB+x]           

                Virtual: Max(1MB, reachable objects’ set (= GC.GetTotalMemory))           

                Physical:            Max(1MB, reachable objects’ set (= GC.GetTotalMemory))           

                VM [32MB] or Large Files Area [1GB]?     VM

    * STACKs, for each thread (even if app is not multithreaded, at least 2/3 threads)  

                Virtual:  At least 64KB    

                Physical:            At least 64KB    

                VM [32MB] or Large Files Area [1GB]?     VM

    * NATIVE DLLs

    (such as sqlceme30.dll, which is P/Invoked by v3.1’s System.Data.SqlServerCe. dll) 

                Virtual:  File size x 150% (uncompressed)

                Physical:            Assume file size x 75%  

                VM [32MB] or Large Files Area [1GB]?     VM

    TOOLS:

    0- DumpMem: http://support.microsoft.com/kb/326164

    1- NETCF v2 SP2’s RPM will give you the new opportunity to take multiple pictures of the managed heap (“View GC Heap” button) during application lifetime and prepare some statistics for you to compare different moments.

    Finding Managed Memory leaks using the .Net CF Remote Performance Monitor

    2- FxCop might be a good tool if RPM don’t help (http://www.gotdotnet.com/team/fxcop): to use with NETCF see http://blog.opennetcf.org/ncowburn/PermaLink,guid,aa3bec5c-94d7-48df-95f4-89c2c87211eb.aspx.

    When using FxCop, search for messages whose TypeName="TypesThatOwnDisposableFieldsShouldBeDisposable"

    --> Implementing Finalize and Dispose to Clean Up Unmanaged Resources

    This is something you need to take care of when developing UserControls that include for example a graphical object.

    3- Finally with 3.5 you can use CLR Profiler to identify code that causes memory problems, such as memory leaks and excessive or inefficient garbage collection. However, this is available only for NETCF v3.5 applications. Surely more on this on a later post.

    4- Since SSCE can also run on desktops, if the NETCF application doesn’t use special components (barcode scanner, for example) or doesn't P/Invoke Windows CE-specific APIs, it might be interesting to run it against the DESKTOP runtime, purely because there are many profiling tools – starting for example with the ones available within VS2005 Team System:

    Analyzing Application Performance (Visual Studio Team System)

     

     

    I couldn't terminate a post about NETCF + Memory without mentioning the 2 following readings:

    - Mike Zintel's .Net Compact Framework Advanced Memory Management  

    - Steven Pratschner's An Overview of the .Net Compact Framework Garbage Collector

    CONCLUSIONS: If you have ever troubleshoot-ed a memory leak in a NETCF application, you'll probably know how to design its next version... ;-)

    -----------------------------------

    Appendix:

    When you don’t know yet if the problem is with the Virtual memory or with the Physical Memory (RAM), you can monitor both by P/Invoke-ing the GlobalMemoryStatus() API within the lifetime of the application. The code you should insert might be similar to the following:

    namespace MemoryStatus
    {
        public class MemoryStatus
       {
            [DllImport("coredll.dll")]
            public static extern void GlobalMemoryStatus(ref MEMORYSTATUS lpBuffer);
    
            public struct MEMORYSTATUS
            {
                public int dwLength;
                public int dwMemoryLoad;
                public int dwTotalPhys;
                public int dwAvailPhys;
                public int dwTotalPageFile;
                public int dwAvailPageFile;
                public int dwTotalVirtual;
                public int dwAvailVirtual;
            };
    
            const string CRLF = "\r\n";
            public static string GetStatus()
            {
                MEMORYSTATUS ms = new MEMORYSTATUS();
                ms.dwLength = Marshal.SizeOf(ms);
                GlobalMemoryStatus(ref ms);
                string strAppName = "Memory Status";
                StringBuilder sbMessage = new StringBuilder();
                sbMessage.Append("Memory Load = ");
                sbMessage.Append(ms.dwMemoryLoad.ToString() + "%");
                sbMessage.Append(CRLF);
                sbMessage.Append("Total RAM = ");
                sbMessage.Append(ms.dwTotalPhys.ToString("#,##0"));
                sbMessage.Append(CRLF);
                sbMessage.Append("Avail RAM = ");
                sbMessage.Append(ms.dwAvailPhys.ToString("#,##0"));
                sbMessage.Append(CRLF);
                sbMessage.Append("Total Page = ");
                sbMessage.Append(ms.dwTotalPageFile.ToString("#,##0"));
                sbMessage.Append(CRLF);
                sbMessage.Append("Avail Page = ");
                sbMessage.Append(ms.dwAvailPageFile.ToString("#,##0"));
                sbMessage.Append(CRLF);
                sbMessage.Append("Total Virt = ");
                sbMessage.Append(ms.dwTotalVirtual.ToString("#,##0"));
                sbMessage.Append(CRLF);
                sbMessage.Append("Avail Virt = ");
                sbMessage.Append(ms.dwAvailVirtual.ToString("#,##0"));
                return sbMessage.ToString();
            }
        }
    }
Page 1 of 1 (7 items)