Mobile Development-PingAlert: watch your servers

Are you looking for a tool that periodically pings a list of hosts? Here is a toolset that will reschedule a ping utilitiy. This ping utility pings a list of hosts and will create notifications if one or most hosts failed to answer.

I have done this toolset with three separate applications. The scheduler and the notification tool are written in C/C++ win32 API cause this API provided the best access to all the possibilities of the used functions. The user notification API is only supported with basic functionality by CF2. The CEUserNotification API is not supported by CF2 at all.

Although OpenNetCF provides an C# interface to the used APIs, I did not like to include all the unneeded stuff. On the other hand the scheduler is fast and small written in C/C++. With C# I had problems with the notification, especially for removing existing notifications and why should the main tool reside in memory just for showing the notification.

The ping tool is written in C# targeting Compact Framework 2. C# was the easiest to implement the GUI.

All source is available thru one Visual Studio 2008 solution. Yes, you can mix C/C++ and CF2 within one solution.

The code of the scheduler called PingAlertScheduler is based on my code How to run an application periodically. The code first clears all existing schedules of themself and then creates a new schedule for xx minutes in the future. If the scheduler is run from the schedule event, it will get a special arg transmitted “PingAlert”

...
    TCHAR szExtApp[] = L"\\Windows\\PingAlert.exe";
    TCHAR szExtArg[] = L"pingsweep";
    TCHAR szScheduleArg[] = L"PingAlert";
...
    if (_wcsicmp(szScheduleArg, lpCmdLine)==0)
    {

        nclog(L"PingAlertScheduler: processing CmdLine=%s...\n", szScheduleArg);
        nclog(L"PingAlertScheduler: ...will wakeup again after reschedule...\n");
        //schedule next run
        if ( !FAILED(ScheduleRunApp(lpFileName, szScheduleArg)) )
        {
            //the worker application should be launched here!
            nclog(L"PingAlertScheduler: starting target app %s...\n", szExtApp);
            //is the target already running, then send WM_USER msg
            //else start a new instance with cmdLine="pingsweep"
            PROCESS_INFORMATION pi;
            if( CreateProcess(szExtApp,szExtArg,
                    NULL,NULL,NULL, 0, NULL,NULL,NULL,
                    &pi)!=0)
            {
                nclog(L"PingAlertScheduler: CreateProcess for '%s' OK\n", szExtApp);
                CloseHandle(pi.hThread);
                CloseHandle(pi.hProcess);
            }
            else
                nclog(L"PingAlertScheduler: CreateProcess for '%s' FAILED: %u\n", szExtApp, GetLastError());
            //inform target
            //iRet=signalEvent();
            goto MainExit;
        }
        else{
            MessageBox(NULL, L"error in ScheduleRunApp", lpFileName, MB_TOPMOST | MB_SETFOREGROUND);
            nclog(L"PingAlertScheduler: error in ScheduleRunApp\n");
            iRet=-2;
            goto MainExit; //OK
        }
    }

This code also calls the reschedule to run itself again in the specified time interval.

static HRESULT ScheduleRunApp(
  LPCTSTR szExeName,
  LPCTSTR szArgs)
{
    //do not add a schedule if actual date is 21.3.2003
    SYSTEMTIME t;
    memset(&t, 0, sizeof(SYSTEMTIME));
    GetLocalTime(&t);
    //check if the system clock is at factory default, device specific!
    if ( (t.wYear == 2003) && (t.wMonth == 3) && (t.wDay == 21) )
    {
        nclog(L"PingAlertScheduler: # no next run schedule as date is 21.03.2003!\n");
        return NOERROR;
    }

    HRESULT hr = S_OK;
    HANDLE hNotify = NULL;

    // set a CE_NOTIFICATION_TRIGGER
    CE_NOTIFICATION_TRIGGER notifTrigger;
    memset(&notifTrigger, 0, sizeof(CE_NOTIFICATION_TRIGGER));
    notifTrigger.dwSize = sizeof(CE_NOTIFICATION_TRIGGER);

    // calculate time
    SYSTEMTIME st = {0};
    GetLocalTime(&st);

    st = AddDiff(&st, iScheduleInterval); //wake in x minutes
    wsprintf(str, L"Next run at: %02i.%02i.%02i %02i:%02i:%02i\n",
                                        st.wDay, st.wMonth , st.wYear,
                                        st.wHour , st.wMinute , st.wSecond );
    nclog(L"PingAlertScheduler: %s\n", str);

    notifTrigger.dwType = CNT_TIME;
    notifTrigger.stStartTime = st;

    // timer: execute an exe at specified time
    notifTrigger.lpszApplication = (LPTSTR)szExeName;
    notifTrigger.lpszArguments = (LPTSTR)szArgs;

    hNotify = CeSetUserNotificationEx(0, &notifTrigger, NULL);
    // NULL because we do not care the action
    if (!hNotify) {
        hr = E_FAIL;
        nclog(L"PingAlertScheduler: CeSetUserNotificationEx FAILED...\n");
    } else {
        // close the handle as we do not need to use it further
        CloseHandle(hNotify);
        nclog(L"PingAlertScheduler: CeSetUserNotificationEx succeeded...\n");
    }
    return hr;
}

When PingAlertScheduler.exe ist started with “PingAlert” it will also launch the second application of the toolset called PingAlert. The Compact Framework application PingAlert can be started normally to manage the hosts list and it can be started with the argument “pingsweep” and then will automatically start to ping all hosts and then exits itself. If there were ping errors, PingAlert will create a HTML file with a report and then start the notification application of this toolset, which is called PingAlertToast.

How do we handle program arguments in compact framework. See program.cs:

        [MTAThread]
        static void Main(String[] args)
        {
            Application.Run(new Form1(args));
        }

and then I change the form constructor code to receive the args string list:

        public Form1(String[] args)
        {
            InitializeComponent();

            paSettings = new PingAlertSettings();
            readSettings();

            //get cmdLine
            String myArg="";
            if (args.Length == 1)
            {
                myArg = args[0];
                if (myArg.Equals("pingsweep", StringComparison.OrdinalIgnoreCase))
                    bRunAndQuit = true;
            }

At the end of pinging the host list, another boolean signals a timer tick event handler, that it is time to quit the app.

        private void exitTimer1_Tick(object sender, EventArgs e)
        {
            //is it OK to quit now
            if(bQuitIsOK){
                exitTimer1.Enabled = false;
                this.Close();//exit form
            }
        }

At the end of the host pings, the third app, PingAlertToast will be started, if there was at least one error:

...
            if (bte.qData._iCount == numberOfQueuedData)
            {
                //end html
                closeHTML();
                //write report file
                //System.IO.TextWriter stringWriter = new System.IO.StringWriter();
                try
                {
                    System.IO.TextWriter streamWriter = new System.IO.StreamWriter(szHtmlFile);
                    streamWriter.WriteLine(sbHTML);
                    streamWriter.Flush();
                    streamWriter.Close();
                }
                catch (Exception x)
                {
                    addLog("Exception in write HTML: " + x.Message);
                }

                //start notification
                showNotification();
                bQuitIsOK = true;
            }
...

Ah, what is this qData? To have the GUI still responsive during the ping process, the PingAlert uses a queue to add hosts to ping. The background process reads this queue and ping one by the other as long as there are hosts in the queue.

bgThread2.cs:

...
    //The blocking function...
    if (_theQueue.Count > 0)
    {
        //dequeue one IP to ping
        lock (_theQueue.SyncRoot)// syncedCollection.SyncRoot)
        {
            _qData = (queueData)_theQueue.Dequeue();// get object from queue
        }

        //System.Net.IPAddress ip;
        try
        {
            _qData.IP = System.Net.Dns.Resolve(_qData.sHost).AddressList[0];
            iPreplies = myPing.PingQdata(ref _qData);
        }
...

To be able to have some extra information with the hosts I use a class called QueueData which will hold the hosts to ping, the number of ping retries etc.. You can add hosts using there DNS name or there IP address.

How can you you disable the schedule? As you know, PingAlertScheduler can be launched with different args. If it is launched without args, it will create a new schedule and terminates itself. If it is launched with “PingAlert” it will reschedule itself and launch PingAlert with the arg “pingsweep”. If it is launched with the argument “clear”, it will clear all existing schedules to PingAlertScheduler.

So, if you want to stop the scheduling, you have to launch PingAlertScheduler with the argument “clear”. To do so, you can simply use the button “Clear Schedule” in the main PingAlert application.

If you need to start the schedule, you use the button “Schedule”. It will simply launch PingAlertSchedule without an argument.

Now to the last of the trio, PingAlertToast. It is written in C/C++ and creates a user notification. You will get a nice symbol in the taskbar and if you click (tap) it, it will show the notification. This is only a reminder notification signaling that some ping failed.

If you dont like to keep the notification, just tap (Dismiss). To only hide it tap on (Hide). To get the notification back on screen, tap the small symbol in the taskbar.

Within the notification text is a link which should open your internet browser on the device and that shows the report.

The notification API is very strong and enables you to configure nearly everything of the notification. As the API is not fully available in Compact Framework, I did code PingAlertToast in C/C++. Here is the main part creating the notification:

int showNotification(TCHAR* szText){
	LRESULT lRes=0;
    // This code will add an SHNotification notificaion
    SHNOTIFICATIONDATA sn  = g_shNotificationData;
    sn.cbStruct = sizeof(sn);
    sn.dwID = g_NotificationID;
    sn.npPriority = SHNP_INFORM;
    sn.csDuration = -1;
    sn.hicon = LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_SAMPLEICON));
    sn.clsid = guidNotifyApp;
    sn.grfFlags = 0;
    sn.pszTitle = TEXT("PingAlert");
	sn.pszHTML = TEXT("<html><body>There was at least one error. Please click <a href=\"file:///Windows\\PingAlertReport.html\">PingAlert Report</a> for more details.</body></html>");
    sn.rgskn[0].pszTitle = TEXT("Dismiss");
    sn.rgskn[0].skc.wpCmd = g_dwNotificationCmdID1;
	sn.rgskn[0].skc.grfFlags = NOTIF_SOFTKEY_FLAGS_STAYOPEN;
    sn.rgskn[1].pszTitle = TEXT("Hide");
    sn.rgskn[1].skc.wpCmd = g_dwNotificationCmdID2;
	sn.rgskn[1].skc.grfFlags = NOTIF_SOFTKEY_FLAGS_HIDE;
	sn.hwndSink=g_hWnd;
    //Add the notification to the tray
    lRes = SHNotificationAdd(&sn);

    return lRes;
}

Take a deeper look at PingAlertToast.cpp to see more details on handling menu taps etc.

Not to forget, the settings are persistent in the registry at HKLM\Software\PingAlert

REGEDIT4

[HKEY_LOCAL_MACHINE\Software\PingAlert]
"Log2File"=dword:00000000
"TimeInterval"=dword:0000000F
"Hosts"=hex(7):\
67,6F,6F,67,6C,65,2E,63,6F,6D,00,31,39,32,2E,31,36,38,2E,31,32,38,2E,35,\
00,73,6D,61,72,74,00,00
  • Log2File specifies if you like to have a log file for PingAlert’s action in \PingAlert.Log.txt.
  • TimeInterval specifies the interval between reschedules.
  • Hosts is a Multi_SZ registry key holding the list of hosts to ping.

Code at code.google.com

Executable set (copy all into \Windows dir on the device): [Download not found]

Installable CAB file (will place a link in your program folder):  New version 1.1 4. March 2011: [Download not found]