5. How can I get debug information from my program in the field ?
A little history: I've been working in this industry since
CP/M days, and have noticed that it seems an inescapable fact that a certain small
percentage of bugs just don't show up in testing, they only ever show in the field with
real users hitting on the system. This is especially true of real-time systems, which is
the field I started in (embedded military stuff).
Once you accept this fact of life, you realize that for this class of bugs all the ASSERTs
in the world aren't gonna help you because you're working with release builds, and you
need something you can use in the field.
So, to take an example: I have a telephony system which is my current large project. the
system is around a million lines, of which maybe 1/4 are mine. For the main call handling
code I have bits embedded in the code which do something like this :
if (g_bDebugEnabled)
{
wsprintf (m_szDebug, "Mainfrm_LC: max WS=%d max units=%d",
wMaxCallWS,
byNoOfUnits);
DebugMessage (m_szDebug) ;
}
where g_bDebugEnabled is a global flag set up from the registry and m_szDebug is a
suitably large char array.
DebugMessage is the debug handler, which can be as simple or complex as you
like, but would typically look something like this:
BOOL CMainFrame::DebugMessage (const char *
pszString)
{
static char szLocalDebug [MAX_DEBUG_LENGTH+12] ;
LRESULT litem ;
time_t TimeNow ;
struct tm * pTime ;
if (!m_bStoppedData)
{
memset (szLocalDebug, 0, sizeof(szLocalDebug)) ;
TimeNow = time (NULL) ;
pTime = localtime (&TimeNow) ;
strftime (szLocalDebug, 12, "%H:%M:%S >",
pTime) ;
strcat (szLocalDebug, pszString) ;
SendDlgItemMessage (IDC_DEBUG_MSGLIST,
LB_ADDSTRING,
0,
(LPARAM) (LPSTR) szLocalDebug) ;
if (m_bCopyToDbwin)
{
strcat (szLocalDebug, "
<UserInf>\r\n");
OutputDebugString (szLocalDebug) ;
}
litem = SendDlgItemMessage (IDC_DEBUG_MSGLIST,
LB_GETCOUNT,0,0L);
if (litem > MAX_DEBUG_MESSAGES)
litem = SendDlgItemMessage
(IDC_DEBUG_MSGLIST,
LB_DELETESTRING,0,0L);
SendDlgItemMessage (IDC_DEBUG_MSGLIST,
LB_SETCURSEL,
(WORD)(litem-1),0L);
if (*pszString == '*') // allow automatic "hold"
Hold (TRUE) ;
return TRUE ;
}
return FALSE ;
}
If you already have a suitable dialog hanging around,
you only have to add a listbox, and a mechanism for holding and clearing it (my diaog is
normally hidden, made visible by a weird keystroke sequence). Having the ability to tell
the debug code to copy its output to OutputDebugString helps, because I can then use a
debug output catcher application like DBWIN32, which provides the ability to save to a
file, and also allows me to catch the debug output from multiple apps, all neatly
serialised. I developed a multi-application serial comms server some time ago, and could
never have met the deadline without this technique to catch timing-related problems.
You pay a little price in code fat for the debug function and the wsprintf code, but if
all you're doing is stuff like tellbacks (i.e. "called this function",
"exited this function") then even that isn't much, and when debug is disabled
you're only paying the performance penalty of a boolean check, which is not much.
Once you have the basic technique down pat, you can then add extra features, like adding
an OnCommand handler which calls DebugMessage to give you a record of all the users
actions, so you can tell exactly what they did, like this one:
BOOL CMainFrame::OnCommand(WPARAM wParam, LPARAM lParam)
{
TCHAR szLabel [32] ;
HMENU hMenu = 0;
if (g_bDebugEnabled)
{
hMenu = ::GetMenu (g_hUIMainWnd);
if (hMenu)
{
if (::GetMenuString (hMenu,
LOWORD(wParam),
szLabel,
sizeof(szLabel)-1,
MF_BYCOMMAND))
{
wsprintf (m_szDebug,
"Menu: User selected '%s'", szLabel);
DebugMessage
(m_szDebug);
}
}
}
return CFrameWnd::OnCommand(wParam, lParam);
}
You can add similar handlers to dialogs to capture button pushes etc. It's up to you how
far you want to take it.
Author | Matt J. Scully |