OTStreamLogViewer.c

/*
    File:       StreamLogWatcher.c
 
    Contains:   A program to display information logged to the STREAMS log module.
 
    Written by: Quinn "The Eskimo!"
 
    Copyright:  © 1998-2000 by Apple Computer, Inc., all rights reserved.
 
    Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple Computer, Inc.
                ("Apple") in consideration of your agreement to the following terms, and your
                use, installation, modification or redistribution of this Apple software
                constitutes acceptance of these terms.  If you do not agree with these terms,
                please do not use, install, modify or redistribute this Apple software.
 
                In consideration of your agreement to abide by the following terms, and subject
                to these terms, Apple grants you a personal, non-exclusive license, under AppleÕs
                copyrights in this original Apple software (the "Apple Software"), to use,
                reproduce, modify and redistribute the Apple Software, with or without
                modifications, in source and/or binary forms; provided that if you redistribute
                the Apple Software in its entirety and without modifications, you must retain
                this notice and the following text and disclaimers in all such redistributions of
                the Apple Software.  Neither the name, trademarks, service marks or logos of
                Apple Computer, Inc. may be used to endorse or promote products derived from the
                Apple Software without specific prior written permission from Apple.  Except as
                expressly stated in this notice, no other rights or licenses, express or implied,
                are granted by Apple herein, including but not limited to any patent rights that
                may be infringed by your derivative works or by other works in which the Apple
                Software may be incorporated.
 
                The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO
                WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
                WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
                PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
                COMBINATION WITH YOUR PRODUCTS.
 
                IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
                CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
                GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
                ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
                OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT
                (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
                ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
    Change History (most recent first):
*/
 
#define qDebug 1
 
/////////////////////////////////////////////////////////////////////
 
// Standard C stuff.
 
#include <stdio.h>
 
// Pick up standard OT APIs.
 
#include <OpenTransport.h>
 
// Pick up standard toolbox APIs.
 
#include <QuickDraw.h>
#include <Fonts.h>
#include <Windows.h>
#include <Menus.h>
#include <TextEdit.h>
#include <Dialogs.h>
#include <Memory.h>
#include <Resources.h>
#include <TextUtils.h>
#include <Scrap.h>
#include <Devices.h>
#include <CodeFragments.h>
#include <Gestalt.h>
#include <Processes.h>
#include <InternetConfig.h>
#include <Lists.h>
 
// MoreIsBetter modules
 
#include "MoreAppleEvents.h"
#include "MoreMemory.h"
#include "MoreQuickDraw.h"
#include "MoreAppearance.h"
#include "MoreWindows.h"
#include "MoreDialogs.h"
#include "MoreMenus.h"
#include "MoreLists.h"
#include "MoreErrors.h"
 
// Pick up our resource definitions.
 
#include "StreamLogResources.h"
 
// Pick up our memory management module.
 
#include "OTMemoryReserve.h"
 
// Pick up the file logging stuff.
 
#include "FileLogging.h"
 
// Pick up the log engine prototypes.
 
#include "LogEngine.h"
 
/////////////////////////////////////////////////////////////////////
 
// OTDebugStr is defined in a wacky OT library.  Let's just use 
// lowercase debugstr instead.
 
#define OTDebugStr debugstr
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** Applications Globals *****
 
enum {
    kCreator = 'SlVw'
};
 
static Boolean gQuitNow;
    // Set to true to drop out of the main event loop.
    
static DialogPtr gMainWindow = nil;
    // The main window.  Pretty much the only window at
    // the moment.
    
static ListHandle gLogList = nil;
    // The List Manager list in the main window.
 
static ListDefUPP gListDefProcUPP;
    // A UPP for the list definition proc we use
    // for this list.  We have a dummy LDEF whose
    // resource ID we supply to LNew.  That LDEF just
    // calls through the list's refCon (assuming it isn't
    // nil).  We poke this UPP into that refCon, and hence
    // the LDEF calls us.  This allows for source
    // level debugging of the LDEF without messing around
    // with self-modifying code.
 
static UserItemUPP gListUserItemUpdateUPP;
    // A UPP for the user item in gMainWindow that contains
    // the gLogList.  This routine simply calls LUpdate
    // (oh, and also draws the frame for the list).
 
static Rect gMainWindowGrowBounds;
    // Bounds which we pass to GrowWindow to constrain how
    // much the window can grow or shrink.  The topLeft
    // co-ordinate in the minimum size (which we get from
    // the window's portRect just after we create it)
    // and the botRight are derived from screenBits.bounds
    // (which Window Manager special cases to allow windows
    // to grow as large as possible on the screen(s)).
    
static Boolean gScrollWhileLogging = true;
    // This value is a mirror of the state of the menu
    // item.  We mirror it to make it quicker to fetch while
    // we're logging.
    
static Boolean gStartOnLaunch = false;
    // This value is a mirror of the state of the menu
    // item.
    
static SInt16 gModuleID;                        // -1 => log all modules
static SInt16 gStreamID;                        // -1 => log all streams
    // These are the logging filters.  This only take effect
    // if the All Traces radio button is /not/ on.  The values
    // are set by means of a modal dialog.
 
static void DirtyPreferences(void);
    // forward
static void PreferencesIdle(void);
    // forward
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** Apple Event Handlers *****
 
static pascal OSErr AEOpenApplicationHandler(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon)
{
    #pragma unused(reply)
    #pragma unused(handlerRefcon)
    OSStatus err;
    
    err = MoreAEGotRequiredParams(theAppleEvent);
    return err;
}
 
static pascal OSErr AEOpenDocumentsHandler(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon)
{
    #pragma unused(reply)
    #pragma unused(handlerRefcon)
    OSStatus err;
    
    err = MoreAEGotRequiredParams(theAppleEvent);
    return err;
}
 
static pascal OSErr AEQuitApplicationHandler(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon)
{
    #pragma unused(reply)
    #pragma unused(handlerRefcon)
    OSStatus err;
    
    err = MoreAEGotRequiredParams(theAppleEvent);
    if (err == noErr) {
        gQuitNow = true;
    }
    return err;
}
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** List Logging Stuff *****
 
enum {
 
    // kMaxListRows is the maximum number of rows we can have in our
    // scrolling list.  List Manager imposes an overall limit of 32K.
    // It also imposes a 32K limit on the amount of data in the list.
    // We use 4 bytes per row of data, which would imply this limit
    // should be 8K.  5000 is just to keep it within reasonable bounds.
    
    kMaxListRows = 5000,
    
    // When the list exceeds the above limit, we delete some rows from
    // the front of the list.  We do this in chunks for efficiency's
    // sake.  This is the size of the chunk we delete.
    
    kNumberOfRowsToDelete = 100
};
 
static LogEntryPtr ListGetLogEntryForRow(SInt16 rowNumber)
    // Given a row number (zero based), this routine returns
    // the log entry for that row.  if there is no log entry
    // (which can happen if the routine is called by the LDEF
    // as the row is added), it returns nil.
{
    Cell thisCell;
    LogEntryPtr result;
    SInt16 dataSize;
    
    thisCell.h = 0;
    thisCell.v = rowNumber;
    dataSize = sizeof(result);
    LGetCell(&result, &dataSize, thisCell, gLogList);
    if ( dataSize != sizeof(result) ) {
        result = nil;
    }
    return result;
}
 
static void ListSetLogEntryForRow(SInt16 rowNumber, LogEntryPtr rowEntry)
    // This routine sets the data for the given rowNumber (zero based)
    // to rowEntry, to later be retrieved by ListGetLogEntryForRow.
{
    Cell thisCell;
    
    thisCell.h = 0;
    thisCell.v = rowNumber;
    LSetCellWhite(&rowEntry, sizeof(rowEntry), thisCell, gLogList);
}
 
static void FlagsToShortString(char flags, Str255 flagStr)
    // This routine sets flagStr to an 8 character string
    // that represents flags in a very compact form, suitable
    // for drawing by the LDEF.
{
    OTMemset(&flagStr[1], '-', 8);
    flagStr[0] = 8;
    
    if ((flags & SL_TRACE) != 0) {
        flagStr[1] = 'T';
    }
    if ((flags & SL_ERROR) != 0) {
        flagStr[2] = 'E';
    }
    if ((flags & SL_CONSOLE) != 0) {
        flagStr[3] = 'C';
    }
    
    // flagStr[4] is blank
    
    if ((flags & SL_FATAL) != 0) {
        flagStr[5] = 'F';
    }
    if ((flags & SL_NOTIFY) != 0) {
        flagStr[6] = 'N';
    }
    if ((flags & SL_WARN) != 0) {
        flagStr[7] = 'W';
    }
    if ((flags & SL_NOTE) != 0) {
        flagStr[8] = 'N';
    }
}
 
// These constants define the start (and hence the width)
// of each of the fields in our LDEF.
 
enum {
    kFieldStart0 = 4,
    kFieldStart1 = kFieldStart0 + 40,
    kFieldStart2 = kFieldStart1 + 60,
    kFieldStart3 = kFieldStart2 + 80,
    kFieldStart4 = kFieldStart3 + 80,
    kFieldStart5 = kFieldStart4 + 40,
    kFieldStart6 = kFieldStart5 + 40,
    kFieldStart7 = kFieldStart6 + 0
};
 
static void ListDefProcDraw(Rect *theCellRect, SInt16 rowNumber)
    // This routine is called by the LDEF in response to an
    // lDraw message.  The basic idea is to get the log entry
    // for the given row and then use the given rectangle to
    // render the data in the entry.
{
    LogEntryPtr thisEntry;
    Str255 tmpStr;
    UInt32 strLen;
 
    thisEntry = ListGetLogEntryForRow(rowNumber);
    OTAssert("ListDefProc: No data for cell", thisEntry != nil);
    if (thisEntry != nil) {
 
        NumToString(thisEntry->fLogHeader.seq_no, tmpStr);
        MoveTo(theCellRect->left + kFieldStart0, theCellRect->bottom - 4);
        DrawString(tmpStr);
 
        MoveTo(theCellRect->left + kFieldStart1, theCellRect->bottom - 4);
        FlagsToShortString(thisEntry->fLogHeader.flags, tmpStr);
        DrawString(tmpStr);
 
        MoveTo(theCellRect->left + kFieldStart2, theCellRect->bottom - 4);
        DateString(thisEntry->fLogHeader.ttime, shortDate, tmpStr, nil);
        DrawString(tmpStr);
 
        TimeString(thisEntry->fLogHeader.ttime, true, tmpStr, nil);
        MoveTo(theCellRect->left + kFieldStart3, theCellRect->bottom - 4);
        DrawString(tmpStr);
        
        NumToString(thisEntry->fLogHeader.mid, tmpStr);
        MoveTo(theCellRect->left + kFieldStart4, theCellRect->bottom - 4);
        DrawString(tmpStr);
 
        NumToString(thisEntry->fLogHeader.sid, tmpStr);
        MoveTo(theCellRect->left + kFieldStart5, theCellRect->bottom - 4);
        DrawString(tmpStr);
 
        // We clip strLen at 255 to avoid attempting to draw more than
        // a 255 character string (which won't work 'cause it's a Pascal
        // string).  We could have used DrawText, but 255 characters is
        // enough for me.  If you need more, look at the file log.
        
        strLen = thisEntry->fTextLength;
        if ( strLen > 255 ) {
            strLen = 255;
        }
        BlockMoveData((char *) thisEntry + sizeof(LogEntry), &tmpStr[1], strLen);
        tmpStr[0] = strLen;
        MoveTo(theCellRect->left + kFieldStart6, theCellRect->bottom - 4);
        DrawString(tmpStr);
    }
}
 
static pascal void ListDefProc(SInt16 message, Boolean drawSelected,
                                Rect *theCellRect, Cell theCell, 
                                SInt16 dataOffset, SInt16 dataLen, 
                                ListRef listH)
    // Our list definition proc (LDEF).  This routine is called
    // back by the List Manager when it wants to draw (or highlight)
    // a cell in the list in our main window.  The implementation
    // basically dispatches based on message.  Of cours, there's only
    // one basic action, so this is just a glorified wrapper for
    // ListDefProcDraw.
{
    #pragma unused(dataOffset)
    #pragma unused(dataLen)
 
    #if qDebug
        OTAssert("ListDefProc: This LDEF only works for gLogList", listH == gLogList);
    #else
        #pragma unused(listH)
    #endif
    
    switch (message) {
        case lInitMsg:
            // do nothing
            break;
        case lDrawMsg:
        case lHiliteMsg:
            EraseRect(theCellRect);
            ListDefProcDraw(theCellRect, theCell.v);
            if (drawSelected) {
                MoreSetMagicHiliteMode();
                InvertRect(theCellRect);
            }
            break;
        case lCloseMsg:
            // do nothing
            break;
        default:
            OTDebugStr("ListDefProc: Unrecognised message");
            break;
    }
}
 
static pascal void ListUserItemUpdateProc(WindowPtr dlg, short item)
    // A Dialog Manager user item update proc.  This routine
    // is called by the Dialog Manager when it wants to draw
    // the list user item.  We respond by drawing the list frame
    // and then calling through to List Manager to draw the list itself.
{
    OSStatus err;
    OSStatus junk;
    Rect itemRect;
    MoreThemeDrawingState drawState;
    
    // This erase is critical, because List Manager is not smart
    // enough to erase the area below the defined cells when
    // the defined cells don't take up enough space to occupy the
    // entire viewable area.
    
    err = MoreGetThemeDrawingState(&drawState);
    if (err == noErr) {
        junk = MoreNormalizeThemeDrawingState();
        MoreAssert(junk == noErr);
 
        GetDialogItemRect(dlg, item, &itemRect);
        EraseRect(&itemRect);
 
        err = MoreSetThemeDrawingState(drawState, true);
    }
    MoreAssertQ(junk == noErr);
    
    // Draw the frame.
    
    InsetRect(&itemRect, -1, -1);
    FrameRect(&itemRect);
    
    // Call List Manager to draw the list.
    
    LUpdateWhite(dlg->clipRgn, gLogList);
}
 
static void RecordLogEntryToList(LogEntryPtr thisEntry)
    // This routine is called at idle time when new list entries
    // are noticed.  The basic idea is to append the new entries
    // as rows at the end of the list.  We also have to deal with
    // keeping the list size within bounds and scrolling the new
    // items into view if the user asked to.
{
    SInt16 newRow;
    SInt32 i;
    LogEntryPtr anEntry;
    Cell tmpCell;
    
    OTAssert("RecordLogEntryToList: gLogList is nil", gLogList != nil);
    
    // First check to see whether we have too many rows in the list.  If so,
    // delete the first kNumberOfRowsToDelete rows, making sure we release
    // the data associated with deleted rows.
    
    if ( ((**gLogList).dataBounds.bottom - (**gLogList).dataBounds.top) > kMaxListRows) {
        for (i = 0; i < kNumberOfRowsToDelete; i++) {
            anEntry = ListGetLogEntryForRow(i);
            OTAssert("RecordLogEntryToList: No list data", anEntry != nil);
            ReleaseLogEntry(anEntry);
        }
        LDelRowWhite(kNumberOfRowsToDelete, 0, gLogList);
    }
    
    // Now add the row, and set the data for the new row.
    // Make sure we call RetainLogEntry to denote the extra
    // reference to this data.
    
    LSetDrawingMode(false, gLogList);
    
    newRow = LAddRowWhite(1, 32767, gLogList);
    RetainLogEntry(thisEntry);
    LSetDrawingMode(true, gLogList);
    ListSetLogEntryForRow(newRow, thisEntry);
 
    tmpCell.h = 0;
    tmpCell.v = newRow;
 
    // Finally, scroll the list to show the new item if the user
    // asked us to.
        
    if ( gScrollWhileLogging ) {
        LScrollWhite(0, newRow, gLogList);
    }
}
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** General User Interface Stuff *****
 
static void SizeMainWindow(SInt16 newWidth, SInt16 newHeight)
    // This routine is used to resize the main window,
    // making sure that the list in the window is also resized
    // to match.
{
    Rect itemRect;
    
    // Round the height to the closest 16 pixel boundary.
    // We need to make sure the list stays a multiple of
    // 16 pixels high (otherwise List Manager behaves
    // suckily), and we do this by making sure the window
    // is always a multiple of 16 pixels high.
    
    newHeight = (newHeight + 8) / 16 * 16 - 1;
    
    // Resize the window.
    
    SizeWindow(gMainWindow, newWidth, newHeight, true);
    
    // Now sync up the dialog item and the list view rect
    // with the new window size.  This took some time to get
    // right.
    
    GetDialogItemRect(gMainWindow, ditLogEntryList, &itemRect);
    itemRect.right = gMainWindow->portRect.right;
    itemRect.bottom = gMainWindow->portRect.bottom;
    SetDialogItemRect(gMainWindow, ditLogEntryList, &itemRect);
    itemRect.right -= 15;
    itemRect.bottom -= 15;
    LSizeWhite(itemRect.right - itemRect.left,
            itemRect.bottom - itemRect.top,
            gLogList);
}
 
static void SetMainWindowVisibility(Boolean isVisible)
    // We use this routine to show and hide the main
    // window, making sure that all the UI elements that
    // depend on this state are kept in sync.
{
    Str255 menuItemText;
    
    OTAssert("SetMainWindowVisibility: No list handle", gLogList != nil);
 
    if (isVisible) {
        ShowWindow(gMainWindow);
        
        if ( ! MoreTitleBarOnScreen(gMainWindow) ) {
            MoveWindow(gMainWindow, 100, 100, false);
        }
        
        GetIndString(menuItemText, rMiscStrings, strHideLogWindow);
    } else {
        HideWindow(gMainWindow);
        GetIndString(menuItemText, rMiscStrings, strShowLogWindow);
    }
    SetMenuItemText( GetMenuHandle(mFile), iShowHideLogWindow, menuItemText);
}
 
static void SetScrollWhileLogging(Boolean scrollWhileLogging)
    // We use this routine to set the scroll while logging state,
    // keeping the UI in sync.
{
    gScrollWhileLogging = scrollWhileLogging;
    CheckItem( GetMenuHandle(mFile), iScrollWhileLogging, gScrollWhileLogging);
}
 
static void SetStartOnLaunch(Boolean startOnLaunch)
    // We use this routine to set the scroll while logging state,
    // keeping the UI in sync.
{
    gStartOnLaunch = startOnLaunch;
    CheckItem( GetMenuHandle(mFile), iStartOnLaunch, gStartOnLaunch);
}
 
static void DisplayError(OSStatus errNum)
{
    Str255 becauseString;
    Str255 errNumStr;
    
    if (errNum != noErr && errNum != userCanceledErr) {
        NumToString(errNum, errNumStr);
        MoreLookupError(rErrorTable, errNum, becauseString);
        ParamText(becauseString, errNumStr, "\p", "\p");
        (void) StopAlert(rErrorAlert, GetOKAlertFilterUPP());
    }
}
 
static void UpdateStartStopUI(void)
    // This routine is used to sync up the enabled state of
    // the start and stop user interface to the current
    // logging state.
{
    Str255 tmpStr;
    
    SetDialogControlEnable(gMainWindow, ditStop, LoggingActive());
    SetDialogControlEnable(gMainWindow, ditStart, ! LoggingActive());
    if ( LoggingActive() ) {
        GetIndString(tmpStr, rMiscStrings, strStopLogging);
    } else {
        GetIndString(tmpStr, rMiscStrings, strStopLogging);
    }
    SetMenuItemText(GetMenuHandle(mFile), iStartStopLogging, tmpStr);
}
 
static void UIStartLogging(void)
    // This routine handles the user clicking on the Start
    // logging button (or the equivalent menu command).  It
    // gathers the logging parameters from the dialog items
    // and then calls the logging back end.
{
    OSStatus err;
    UInt32 traceInfoCount;
    struct trace_ids *traceInfo;
    struct trace_ids traceInfoBuffer;
 
    // Gather the logging parameters from the dialog.
    
    traceInfoCount = 0;
    traceInfo = nil;
    if ( GetDialogControlBoolean(gMainWindow, ditLogTraces) ) {
        traceInfoCount = 1;
        traceInfo = &traceInfoBuffer;
        
        if ( GetDialogControlBoolean(gMainWindow, ditAllTraces) ) {
            traceInfoBuffer.ti_mid = -1;
            traceInfoBuffer.ti_sid = -1;
        } else {
            traceInfoBuffer.ti_mid = gModuleID;
            traceInfoBuffer.ti_sid = gStreamID;
        }
        if ( GetDialogControlValue(gMainWindow, ditTraceLevel) == iAll ) {
            traceInfoBuffer.ti_level = -1;
        } else {
            traceInfoBuffer.ti_level = GetDialogControlValue(gMainWindow, ditTraceLevel) - iLevelOffset;
        }
    }
    
    // If we're supposed to be logging to a file, start the
    // file logging module.
    
    err = noErr;
    if ( GetDialogControlBoolean(gMainWindow, ditLogToFile) ) {
        err = StartFileLogging();
    }
    
    // Start the back end with the parameters we've already
    // worked out.
    
    if (err == noErr) {
        err = StartLogging(GetDialogControlBoolean(gMainWindow, ditLogErrors), traceInfoCount, traceInfo);
    }
    
    // Update the user interface to reflect the change
    // of logging state.
    
    if ( err == noErr ) {
        UpdateStartStopUI();
    }
    
    // Clean up.
    
    if (err != noErr) {
        if ( FileLoggingActive() ) {
            StopFileLogging();
        }
    }
    
    DisplayError(err);
}
 
static void DoIdle(void);
    // forward
 
static void UIStopLogging(void)
    // This routine is called in response to the user
    // clicking on the Stop logging button (or the equivalent
    // menu command).  It basically calls through to the back
    // end to stop the logging process, then flushouts out
    // any remaining log entries to the log list and file,
    // then shuts down file logging if it was started up.
{
    StopLogging();
    
    // Calling DoIdle will flush out any log entries that remain
    // in the LIFO, which is important to do before we close the
    // file.
    
    DoIdle();
    
    if ( FileLoggingActive() ) {
        StopFileLogging();
    }
    UpdateStartStopUI();
}
 
static Boolean UIConfirmAndStop(void)
    // A generic routine which we call whenever we have to
    // perform some action that will change a logging parameter.
    // We don't want the current logging parameters to be out
    // of sync with the user interface, and I'm too lazy to
    // support adjusting the parameters dynamically, so instead
    // we stop logging whenever the user changes a logging parameter.
    // This routine puts up an alert asking whether that's OK,
    // and then stops logging if it is.  It returns true if
    // it's OK to perform a change in logging parameters (either
    // because logging was off, or because we just turned it off).
{
    Boolean result;
    
    result = false;
    if ( LoggingActive() ) {
        if ( CautionAlert(rConfirmStopAlert, GetOKCancelAlertFilterUPP()) == kStdOkItemIndex ) {
            UIStopLogging();
            result = true;
        }
    } else {
        result = true;
    }
    return result;
}
 
static void UIPoseFilterDialog(void)
    // This routine puts up the dialog that lets the user
    // enter trace filtering parameters.  It populates the dialog
    // based on gModuleID and gStreamID, and if the user clicks
    // OK it writes back to those globals.
{
    DialogPtr dlg;
    Str255 tmpStr;
    SInt32 tmpLong;
    SInt16 hitItem;
    
    // Create and prepare the dialog.
    
    dlg = GetNewDialog(rFilterDialog, nil, (WindowPtr) -1);
    if (dlg != nil) {
 
        SetupStandardDialogItems(dlg, kStdOkItemIndex, kStdCancelItemIndex);
        
        SetDialogControlBoolean(dlg, ditAllModules, gModuleID == -1);
        SetDialogControlBoolean(dlg, ditChosenModules, gModuleID != -1);
        if (gModuleID != -1) {
            NumToString(gModuleID, tmpStr);
            SetDialogItemString(dlg, ditModuleID, tmpStr);
        }
 
        SetDialogControlBoolean(dlg, ditAllStreams, gStreamID == -1);
        SetDialogControlBoolean(dlg, ditChosenStreams, gStreamID != -1);
        if (gStreamID != -1) {
            NumToString(gStreamID, tmpStr);
            SetDialogItemString(dlg, ditStreamID, tmpStr);
        }
        
        SelectDialogItemText(dlg, ditModuleID, 0, 32767);
        
        // Run the dialog and respond to user events.
        
        ShowWindow(dlg);
        do { 
            ModalDialog(GetOKCancelModalFilterUPP(), &hitItem);
            switch (hitItem) {
                case ditAllModules:
                case ditChosenModules:
                    ToggleDialogControlBoolean(dlg, ditAllModules);
                    ToggleDialogControlBoolean(dlg, ditChosenModules);
                    break;
                case ditAllStreams:
                case ditChosenStreams:
                    ToggleDialogControlBoolean(dlg, ditAllStreams);
                    ToggleDialogControlBoolean(dlg, ditChosenStreams);
                    break;
                case kStdOkItemIndex:
                case kStdCancelItemIndex:
                    // do nothing
                    break;
                default:
                    OTDebugStr("UIPoseFilterDialog: Weird hitItem");
            }
        } while ( hitItem != kStdOkItemIndex && hitItem != kStdCancelItemIndex );
        
        // If OK, gather the results of the dialog and write
        // it back to the global variables.
        
        if (hitItem == kStdOkItemIndex) {
            
            if ( GetDialogControlBoolean(dlg, ditAllModules) ) {
                gModuleID = -1;
            } else {
                GetDialogItemString(dlg, ditModuleID, tmpStr);
                StringToNum(tmpStr, &tmpLong);
                gModuleID = tmpLong;
            }
 
            if ( GetDialogControlBoolean(dlg, ditAllStreams) ) {
                gStreamID = -1;
            } else {
                GetDialogItemString(dlg, ditStreamID, tmpStr);
                StringToNum(tmpStr, &tmpLong);
                gStreamID = tmpLong;
            }
            
            DirtyPreferences();
        }
        
        DisposeDialog(dlg);
        
    } else {
        DisplayError(memFullErr);
    }
}
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** Log Entry Processing *****
 
// gLoggedCount is used to record the total number of entries
// we have logged.  We use this number to maintain a static text
// item in the dialog.
 
static UInt32 gLoggedCount = 0;
 
// gLastLoggedCount and gLastDroppedCount record the last value
// of these two variables we used to fill out the static text fields.
// We keep a record of this so that, each time around the idle loop,
// we don't go to the trouble of updating the static text items unless
// they have changed.
 
static UInt32 gLastLoggedCount = 0;
static UInt32 gLastDroppedCount = 0;
 
static pascal void RecordLogEntry(LogEntryPtr thisEntry, void *refCon)
    // This routine is a callback, called by the logging module (which
    // we in turn called at idle time), when a new log entry has been
    // found.  We call our two logging subsystems (on screen list
    // and file) to log the entry to the appropriate places.  The
    // routines we call are responsible for retaining thisEntry
    // if they need it to hang around; otherwise it will be disposed
    // when this routine returns.
{
    #pragma unused(refCon)
    
    RecordLogEntryToList(thisEntry);
    RecordLogEntryToFile(thisEntry);
    
    gLoggedCount += 1;
}
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** Menu Handling *****
 
static void AdjustMenus(void)
    // This routine adjusts the menus based on the state of the
    // program in preparation for a call to MenuKey or MenuSelect.
{
    MenuHandle menuH;
    Boolean logAtFront;
    Boolean logHasSelection;
    
    // File
    
    menuH = GetMenuHandle(mFile);
    MoreSetMenuItemEnable(menuH, iClose, FrontWindow() == gMainWindow );
    
    // Edit
    
    menuH = GetMenuHandle(mEdit);
    logAtFront = (FrontWindow() == gMainWindow);
    logHasSelection = (LSelectedLine(gLogList) != -1);
    DisableItem(menuH, iUndo);
    MoreSetMenuItemEnable(menuH, iCut, logAtFront && logHasSelection);
    MoreSetMenuItemEnable(menuH, iCopy, logAtFront && logHasSelection);
    DisableItem(menuH, iPaste);
    MoreSetMenuItemEnable(menuH, iClear, logAtFront && logHasSelection);
    MoreSetMenuItemEnable(menuH, iSelectAll, logAtFront && ! LIsEmpty(gLogList) );
}
 
static void CopySelectedListEntriesToClip(void)
    // This routine is used to implement the Copy and Cut
    // menu commands.  It iterates through the list of selected
    // cells and adds the text from each of them into a handle.
    // It then puts that handle into the system scrap.
{
    OSStatus err;
    Cell listCell;
    Handle clipTextH;
    LogEntryPtr anEntry;
    char *entryAsText;
 
    // Create a handle for the text we're copying.
    
    clipTextH = NewHandle(0);
    err = MoreMemError(clipTextH);
    if (err == noErr) {
    
        // Iterate through the selected cells, adding their
        // text representations to the handle.
        
        listCell.v = 0;
        listCell.h = 0;
        while ( LGetSelect(true, &listCell, gLogList) ) {
 
            anEntry = ListGetLogEntryForRow(listCell.v);
            OTAssert("ClearSelectedListEntries: No list data", anEntry != nil);
 
            entryAsText = LogEntryToCString(anEntry);
            err = PtrAndHand(entryAsText, clipTextH, OTStrLength(entryAsText));
            if (err != noErr) {
                break;
            }
            listCell.v += + 1;
            listCell.h = 0;
        }
    }
    
    // Put the newly created text into the system scrap.
    
    if (err == noErr) {
        (void) ZeroScrap();
        err = PutScrap( GetHandleSize(clipTextH), 'TEXT', *clipTextH );
        if (err > 0) {
            err = 0;
        }
    }
    
    // Clean up.
    
    if ( clipTextH != nil ) {
        DisposeHandle(clipTextH);
    }
    DisplayError(err);
}
 
static void ClearSelectedListEntries(void)
    // This routine is used to implement the Cut and Clear
    // menu commands.  It iterates through the list of selected
    // cells deleting them.
{
    Cell listCell;
    LogEntryPtr anEntry;
    
    // I turn off drawing mode, delete all the selected cells, turn
    // drawing mode back on and then invalidate the entire list.  This
    // causes flashing if there's only one cell selected, but it works
    // much better in the case where there are *lots* of cells selected.
    // I chose to do it this way because I think the latter case will be
    // more prevelant, and I don't have time to handle both cases properly.
 
    LSetDrawingMode(false, gLogList);
    listCell.v = 0;
    listCell.h = 0;
    while ( LGetSelect(true, &listCell, gLogList) ) {
 
        anEntry = ListGetLogEntryForRow(listCell.v);
        OTAssert("ClearSelectedListEntries: No list data", anEntry != nil);
        ReleaseLogEntry(anEntry);
        LDelRowWhite(1, listCell.v, gLogList);
 
        // Don't increment listCell.v because we've just deleted it.
        // LGetSelect will continue looking from the current co-ordinates,
        // which will be the next selected cell.
        // listCell.v += + 1;
        listCell.h = 0;
    }
    LSetDrawingMode(true, gLogList);
    InvalDialogItem(gMainWindow, ditLogEntryList);
}
 
static void DoMenu(SInt32 menuAndItem)
    // Handle a menu command.  menuAndItme is a longint
    // with the menu number in the top 16 bits and the
    // menu item in the bottom 16, ie the format returned
    // by MenuSelect and MenuKey.
    //
    // Yeah, this should be more than one procedure (-:
{
    SInt16 menu;
    SInt16 item;
    UInt32 startTicks;
    UInt32 junkLong;
    Str255 daName;
    SInt16 junk;
    VersRecHndl versH;
    SInt8 s;
    
    startTicks = TickCount();
    
    menu = menuAndItem >> 16;
    item = menuAndItem & 0x0FFFF;
    switch (menu) {
        case mApple:
            switch (item) {
                case iAbout:
                    versH = (VersRecHndl) Get1Resource('vers', 1);
                    OTAssert("", versH != nil);
                    s = HGetState( (Handle) versH );
                    HLock( (Handle) versH );
                    ParamText( (**versH).shortVersion, "\p", "\p", "\p");
                    junk = Alert(rAboutBox, GetOKAlertFilterUPP());
                    HSetState( (Handle) versH, s );
                    break;
                default:
                    GetMenuItemText(GetMenuHandle(mApple), item, daName);
                    junk = OpenDeskAcc(daName);
                    break;
            }
            break;
        case mFile:
            switch (item) {
                case iClose:
                    SetMainWindowVisibility(false);
                    DirtyPreferences();
                    break;
                case iShowHideLogWindow:
                    SetMainWindowVisibility( ! ((WindowPeek) gMainWindow)->visible );
                    DirtyPreferences();
                    break;
                case iStartStopLogging:
                    if ( LoggingActive() ) {
                        FlashDialogControl(gMainWindow, ditStop);
                        UIStopLogging();
                    } else {
                        FlashDialogControl(gMainWindow, ditStart);
                        UIStartLogging();
                    }
                    break;
                case iScrollWhileLogging:
                    SetScrollWhileLogging( ! gScrollWhileLogging);
                    DirtyPreferences();
                    break;
                case iStartOnLaunch:
                    SetStartOnLaunch( ! gStartOnLaunch);
                    DirtyPreferences();
                    break;
                case iQuit:
                    gQuitNow = true;
                    break;
                default:
                    OTDebugStr("DoMenu: Weird File menu item");
                    break;
            }
            break;
        case mEdit:
            OTAssert("DoMenu: Edit menu should be disabled if gMainWindow not at front", gMainWindow == FrontWindow());
            switch (item) {
                case iCut:
                    CopySelectedListEntriesToClip();
                    ClearSelectedListEntries();
                    break;
                case iCopy:
                    CopySelectedListEntriesToClip();
                    break;
                case iClear:
                    ClearSelectedListEntries();
                    break;
                case iSelectAll:
                    LSelectAll(gLogList);
                    break;
                default:
                    OTDebugStr("DoMenu: Weird Edit menu item");
                    break;
            }
        default:
            // OTDebugStr("DoMenu: Weird menu item");
            break;
    }
    
    // Unhighlight the menu, delaying to avoid a quick flash.
    
    if ( ! gQuitNow ) {
        while ( TickCount() < startTicks + 6 ) {
            Delay(1, &junkLong);
        }
        HiliteMenu(0);
    }
}
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** Low Level Event Handling *****
 
static void DoMouseDown(EventRecord *event)
    // Handle a mouse down event.  This is standard
    // Mac OS stuff.  Call FindWindow to determine what
    // type and event it was (and what window it happened in)
    // and then handle each case.
{
    SInt16 partCode;
    WindowPtr hitWindow;
    long growResult;
    
    partCode = FindWindow(event->where, &hitWindow);
    switch ( partCode ) {
        case inGoAway:
            if ( TrackGoAway(hitWindow, event->where) ) {
                if ( hitWindow == gMainWindow ) {
                    SetMainWindowVisibility(false);
                    DirtyPreferences();
                } else {
                    OTDebugStr("DoMouseDown: Does not handle closing other windows");
                }
            }
            break;
        case inMenuBar:
            AdjustMenus();
            DoMenu(MenuSelect(event->where));
            break;
        case inDrag:
            DragWindow(hitWindow, event->where, &qd.screenBits.bounds);
            DirtyPreferences();
            break;
        case inGrow:
            growResult = GrowWindow(hitWindow, event->where, &gMainWindowGrowBounds);
            if ( growResult != 0 ) {
                Rect tmpRect;
                
                if ( hitWindow == gMainWindow ) {
                    SizeMainWindow(growResult, growResult >> 16);
                    DirtyPreferences();
                } else {
                    SizeWindow(hitWindow, growResult, growResult >> 16, true);
                }
                
                tmpRect = hitWindow->portRect;
                tmpRect.top = tmpRect.bottom - 15;
                InvalRect(&tmpRect);
                tmpRect = hitWindow->portRect;
                tmpRect.left = tmpRect.right - 15;
                InvalRect(&tmpRect);
            }
            break;
        case inZoomIn:
        case inZoomOut:
            if ( TrackBox(hitWindow, event->where, partCode) ) {
                ZoomWindow(hitWindow, partCode, false);
                if (hitWindow == gMainWindow) {
                    SizeMainWindow(hitWindow->portRect.right - hitWindow->portRect.left,
                                    hitWindow->portRect.bottom - hitWindow->portRect.top);
                }
                
                // Force everything to redraw.  I know this isn't perfect,
                // but I'd rather have window zooming working but flashy
                // than not have it.
                
                EraseRect(&hitWindow->portRect);
                InvalRect(&hitWindow->portRect);
            }
            break;
        default:
            // do nothing
            break;
    }
}
 
static void DoIdle(void)
    // Called each time through the main event loop.  This routine
    // handles getting the new log entries out of the log module
    // and into RecordLogEntry, which sends them to the file and list
    // logging code.  It also idles the file and preferences modules,
    // which delay actions for a time to prevent unnecessary disk thrashing.
{
    Str255 tmpStr;
    UInt32 droppedCount;
    
    // Don't ask me why, or I'll start to whimper.
 
    OTIdle();
 
    // OK, I'll explain.  Basically there's a weird concurrency
    // hole in OT such that, if you don't have any other network
    // activity, strlog messages will never get delivered
    // to the client.  I've reported this as a bug [Radar ID 2218220],
    // but it's easy to work around (calling OTIdle will deliver
    // the messages) and it's so deep in the OT kernel that 
    // I'm not sure anyone will ever want to take the risk
    // associated with fixing it.
    
    ForEachNewLogEntry(RecordLogEntry, nil);
    
    FileLoggingIdle();
 
    PreferencesIdle();
 
    // Update the static text to reflect the number
    // of logs we've seen and/or dropped.
    
    if ( gLoggedCount != gLastLoggedCount ) {
        NumToString(gLoggedCount, tmpStr);
        SetDialogItemString(gMainWindow, ditLoggedCount, tmpStr);
        gLastLoggedCount = gLoggedCount;
    }
    
    droppedCount = NumberOfDroppedLogEntries();
    if ( droppedCount != gLastDroppedCount ) {
        NumToString(droppedCount, tmpStr);
        SetDialogItemString(gMainWindow, ditDroppedCount, tmpStr);
        gLastDroppedCount = droppedCount;
    }
}
 
static pascal void GetLogListCellText(ListHandle listH, const Cell *listCell, Str255 cellText)
    // This routine is a callback from LDoKey to get the text of
    // a cell.  We return the sequence number for the log entry.
    // This allows LDoKey to implement "select by typing".
{
    #pragma unused(listH)
    LogEntryPtr thisEntry;
    
    cellText[0] = 0;
    
    thisEntry = ListGetLogEntryForRow(listCell->v);
    OTAssert("GetLogListCellText: No data for cell", thisEntry != nil);
    if (thisEntry != nil) {
        NumToString(thisEntry->fLogHeader.seq_no, cellText);
    }
}
 
static void DoUpdateEvent(EventRecord *event)
    // In general we handle update events through
    // the Dialog Manager DialogSelect routine.  However,
    // for the main window, we have to manually draw the
    // grow box.  This is not as easy as it might seem, because
    // we don't want the silly fake scroll bars in our window.
    // We we set the clip region to the bottom right 15 x 15
    // rectangle, so we get the grow box and nothing else.
    //
    // Of course, Mac OS 8 Appearance does silly things here
    // too, because the grow box is now part of the window
    // structure region.  But it recognises the call to DrawGrowIcon
    // and does The Right Thing (tm).
{
    RgnHandle tmpRgn;
    Rect growRect;
 
    if ( (WindowPtr) event->message == gMainWindow ) {
        tmpRgn = NewRgn();
        OTAssert("MainEventLoop: Could not create temporary region", tmpRgn != nil);
        
        CopyRgn(gMainWindow->clipRgn, tmpRgn);
        growRect = gMainWindow->portRect;
        growRect.top = growRect.bottom - 15;
        growRect.left = growRect.right - 15;
        ClipRect(&growRect);
        
        DrawGrowIcon( (WindowPtr) event->message );
        
        SetClip(tmpRgn);
        DisposeRgn(tmpRgn);
    }
}
 
static void DoMainWindowHit(EventRecord *event, SInt16 hitItem, SInt16 oldTraceLevel)
    // Handle a hit on an item in the main dialog window.
    // A big switch state with a bunch of UI twiddling
    // that, if we had a real windowing toolbox, would not
    // be necessary!
    //
    // oldTraceLevel is the old value of the trace level popup
    // menu.  The MainEventLoop saves this value before calling
    // DialogSelect.  DialogSelect will change the popup and
    // tells us that it's done so.  We then ask the user to
    // confirm whether they want to stop logging.  If they say
    // they don't, we reset the popup back to the old value.
{
    Point localWhere;
    
    switch ( hitItem ) {
        case ditStart:
            UIStartLogging();
            break;
        case ditStop:
            UIStopLogging();
            break;
 
        case ditLogErrors:
        case ditLogTraces:
        case ditLogToFile:
            if ( UIConfirmAndStop() ) {
                ToggleDialogControlBoolean(gMainWindow, hitItem);
                DirtyPreferences();
            }
            break;
 
        case ditAllTraces:
        case ditTracesMatching:
            if ( UIConfirmAndStop() ) {
                SetDialogControlBoolean(gMainWindow, ditAllTraces, hitItem == ditAllTraces);
                SetDialogControlBoolean(gMainWindow, ditTracesMatching, hitItem == ditTracesMatching);
                SetDialogControlEnable(gMainWindow, ditSetupFilter, hitItem == ditTracesMatching);
                DirtyPreferences();
            }
            break;
 
        case ditSetupFilter:
            if ( UIConfirmAndStop() ) {
                UIPoseFilterDialog();
            }
            break;
            
        case ditTraceLevel:
            if ( UIConfirmAndStop() ) {
                DirtyPreferences();
            } else {
                SetDialogControlValue(gMainWindow, ditTraceLevel, oldTraceLevel);
            }
            break;
            
        case ditLogEntryList:
            localWhere = event->where;
            GlobalToLocal(&localWhere);
            (void) LClickWhite(localWhere, event->modifiers, gLogList);
            break;
        default:
            OTDebugStr("DoMainWindowHit: Unexpected hitItem");
            break;
    }
}
 
static void MainEventLoop(void)
{
    EventRecord event;
    WindowRef hitWindow;
    SInt16 hitItem;
    SInt16 oldTraceLevel;
 
    if (gStartOnLaunch) {
        // I tried flashing the button but it looks kinda ugly.
        //
        // FlashDialogControl(gMainWindow, ditStart);
        UIStartLogging();
    }
        
    gQuitNow = false;
    do {
        DoIdle();
        
        (void) WaitNextEvent(everyEvent, &event, 60, nil);
 
        SetPort(gMainWindow);
        
        if ( IsDialogEvent(&event) ) {
            oldTraceLevel = GetDialogControlValue(gMainWindow, ditTraceLevel);
            if ( DialogSelect(&event, &hitWindow, &hitItem) ) {
                if (hitWindow == gMainWindow) {
                    DoMainWindowHit(&event, hitItem, oldTraceLevel);
                } else {
                    OTDebugStr("MainEventLoop: What other dialogs?");
                }
            }
        }
        switch (event.what) {
            case keyDown:
            case autoKey:
                if ( event.what == keyDown && (event.modifiers & cmdKey) != 0 ) {
                    AdjustMenus();
                    DoMenu(MenuKey(event.message & charCodeMask));
                } else if ( FrontWindow() == gMainWindow) {
                    UInt8 typedChar;
                    
                    typedChar = (event.message & charCodeMask);
                    if ( typedChar == kClearCharCode ||
                            typedChar == kBackspaceCharCode ||
                            typedChar == kDeleteCharCode ) {
                        ClearSelectedListEntries();
                    } else {
                        LDoKey(gLogList, &event, GetLogListCellText);
                    }
                }
                break;
            case mouseDown:
                DoMouseDown(&event);
                break;
            case updateEvt:
                DoUpdateEvent(&event);
                break;
            case activateEvt:
                if ( (WindowPtr) event.message == gMainWindow ) {
                    LActivateWhite( (event.modifiers & 1) != 0, gLogList);
                }
                break;
            case kHighLevelEvent:
                (void) AEProcessAppleEvent(&event);
                break;
            default:
                // do nothing
                break;
        }
    } while ( ! gQuitNow );
 
    // As we're quitting, we'd better shut down the logging
    // systems.
        
    if ( LoggingActive() ) {
        FlashDialogControl(gMainWindow, ditStop);
        UIStopLogging();
    }
}
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** Prefs Management *****
 
enum {
    kPrefsMagic = kCreator
};
 
// PrefsRecord is stored in Internet Config (if it is
// installed) because it's a handy preferences system
// and I really don't want to create yet another file
// in the Preferences folder.  Because it's stored
// on the disk, it has to be 68K aligned (becasue it might
// be written by a 68K and read by a PPC, or vice versa).
//
// The fields in the record pretty much mirror the state
// in the user interface elements.  We save all this state
// so the user (namely me during testing!) doesn't have to
// reset it each time they launch the program.
 
#pragma options align=mac68k
struct PrefsRecord0 {
    OSType  magic;              // kPrefsMagic
    Rect    mainWindowPosition;
    SInt16  moduleID;
    SInt16  streamID;
    Boolean logToFile;
    Boolean logErrors;
    Boolean logTraces;
    Boolean tracesMatching;
    Boolean mainWindowVisible;
    Boolean scrollWhileLogging;
};
typedef struct PrefsRecord0 PrefsRecord0, *PrefsRecord0Ptr;
 
struct PrefsRecord {
    OSType  magic;              // kPrefsMagic
    Rect    mainWindowPosition;
    SInt16  moduleID;
    SInt16  streamID;
    Boolean logToFile;
    Boolean logErrors;
    Boolean logTraces;
    Boolean tracesMatching;
    Boolean mainWindowVisible;
    Boolean scrollWhileLogging;
    Boolean startOnLaunch;
};
typedef struct PrefsRecord PrefsRecord, *PrefsRecordPtr;
#pragma options align=reset
 
static UInt8 *gICPrefsKey = "\p536C5677¥PrefsRecord";
    // The Internet Config key we use to save the PrefsRecord.
 
static ICInstance gICInstance = nil;
    // Our connection to Internet Config.  Note that
    // gICInstance != nil implies that it's safe to call IC.
 
enum {
    kPrefChangeWriteDelay = 600,
        // The length of time between when the preferences
        // are dirtied and when we write them.  This allows
        // multiple preference writes to be collated into one,
        // thereby saving some disk thrashing.
    
    kPrefsCleanTicks = (UInt32) -(kPrefChangeWriteDelay+1)
        // When the preferences are clean we set gTicksOfLastPrefChange
        // to this value to prevent further writes from happening.
        // This corresponds to "far in the future".
};
 
static UInt32 gTicksOfLastPrefChange = kPrefsCleanTicks;
    // The TickCount time at which we last modified the
    // preferences.  If TickCount is more than this value plus
    // kPrefChangeWriteDelay, we need to save the preferences now.
 
static void RestorePreferences(void)
    // Restore the preferences from Internet Config,
    // and put all the saved values back into the appropriate
    // state holder in the program.
{
    OSStatus err;
    PrefsRecord prefs;
    SInt32 prefsSize;
    ICAttr junkAttr;
    
    OTAssert("RestorePreferences: Need a main window to do this", gMainWindow != nil);
    OTAssert("RestorePreferences: Need a log list to do this", gLogList != nil);
 
    // Start by creating a connection to IC, only if it's present
    // (and we linked to it).
        
    err = noErr;
    if ( (void *) ICStart == (void *) kUnresolvedCFragSymbolAddress ) {
        err = -1;
    }
    if (err == noErr) {
        err = ICStart(&gICInstance, kCreator);
    }
    if (err == noErr) {
        err = ICFindConfigFile(gICInstance, 0, nil);
    }
    
    // Then read the preferences from IC.  Ignore truncated errors.
    // It probably means that the preferences were last written by 
    // a more modern version of this program, but it should damn well
    // make sure that our fields are the first fields of its preferences
    // record.
    
    if (err == noErr) {
        prefsSize = sizeof(PrefsRecord);
        err = ICGetPref(gICInstance, gICPrefsKey, &junkAttr, (void *) &prefs, &prefsSize);
        // err = -666;
        if (err == icTruncatedErr) {
            prefsSize = sizeof(PrefsRecord);
            err = noErr;
        }
    }
    
    // If the prefs record is too small, then look to see if matches 
    // the prefs record of an older version of the program.  If it does, 
    // upgrade the prefs record by filling in the new fields.  Note that 
    // we donÕt dirty the preferences here.  If the user dirties the 
    // preferences for any other reason then weÕll write out an upgraded 
    // prefs record.
    
    if (err == noErr && prefsSize < sizeof(PrefsRecord)) {
        if (prefsSize == sizeof(PrefsRecord0)) {
            prefs.startOnLaunch = false;
        } else {
            err = -2;
        }
    }
 
    // Basical sanity checks.
    
    if (err == noErr && prefs.magic != kPrefsMagic) {
        err = -3;
    }
 
    // If we could not read preferences, use defaults instead.
        
    if (err != noErr) {
        prefs.magic = kPrefsMagic;
        MoreGetWindowGlobalRect(gMainWindow, &prefs.mainWindowPosition);
        prefs.moduleID = 0;
        prefs.streamID = 0;
        prefs.logToFile = false;
        prefs.logErrors = true;
        prefs.logTraces = true;
        prefs.tracesMatching = false;
        prefs.mainWindowVisible = true;
        prefs.scrollWhileLogging = true;
        prefs.startOnLaunch = false;
    }
    
    // Now set up the program state from the preferences.
    
    gModuleID = prefs.moduleID;
    gStreamID = prefs.streamID;
    
    SetDialogControlBoolean(gMainWindow, ditLogToFile, prefs.logToFile);
    SetDialogControlBoolean(gMainWindow, ditLogErrors, prefs.logErrors);
    SetDialogControlBoolean(gMainWindow, ditLogTraces, prefs.logTraces);
    SetDialogControlBoolean(gMainWindow, ditTracesMatching, prefs.tracesMatching);
    SetDialogControlBoolean(gMainWindow, ditAllTraces, ! prefs.tracesMatching);
 
    SetDialogControlEnable(gMainWindow, ditSetupFilter, prefs.tracesMatching);
    
    MoveWindow(gMainWindow, prefs.mainWindowPosition.left, prefs.mainWindowPosition.top, false);
    SizeMainWindow(prefs.mainWindowPosition.right - prefs.mainWindowPosition.left,
                    prefs.mainWindowPosition.bottom - prefs.mainWindowPosition.top);
    SetMainWindowVisibility(prefs.mainWindowVisible);
    SetScrollWhileLogging(prefs.scrollWhileLogging);
    SetStartOnLaunch(prefs.startOnLaunch);
}
 
static void SavePreferences(void)
    // Save the preferences to Internet Config.
{
    OSStatus junk;
    PrefsRecord prefs;
 
    // Don't even think about this if we don't have a connection
    // to IC.
        
    if ( gICInstance != nil ) {
    
        // Fill out the PrefsRecord from the program state
        // and write it out.
        
        prefs.magic = kPrefsMagic;
        MoreGetWindowGlobalRect(gMainWindow, &prefs.mainWindowPosition);
        prefs.moduleID = gModuleID;
        prefs.streamID = gStreamID;
        prefs.logToFile = GetDialogControlBoolean(gMainWindow, ditLogToFile);
        prefs.logErrors = GetDialogControlBoolean(gMainWindow, ditLogErrors);
        prefs.logTraces = GetDialogControlBoolean(gMainWindow, ditLogTraces);
        prefs.tracesMatching = GetDialogControlBoolean(gMainWindow, ditTracesMatching);
        prefs.mainWindowVisible =  ((WindowPeek) gMainWindow)->visible;
        prefs.scrollWhileLogging = gScrollWhileLogging;
        prefs.startOnLaunch = gStartOnLaunch;
        
        junk = ICSetPref(gICInstance, gICPrefsKey, kICAttrNoChange, (void *) &prefs, sizeof(PrefsRecord));
        OTAssert("SavePreferences: Error writing prefs", junk == noErr);
    }
    gTicksOfLastPrefChange = kPrefsCleanTicks;
}
 
static void DirtyPreferences(void)
    // Called by the general code when something that is
    // stored in the prefs is modified.  We note the time
    // this happens, and the idle function should eventually
    // notice this and call SavePreferences.
{
    gTicksOfLastPrefChange = TickCount();
}
 
static void PreferencesIdle(void)
    // Our idle function, called by DoIdle, once each time around
    // the event loop.  We see how long the preferences have been dirty
    // and write them if it's been too long.
{
    if ( TickCount() > gTicksOfLastPrefChange + kPrefChangeWriteDelay) {
        SavePreferences();
    }
}
 
static void TermPreferences(void)
    // This routine is called when the application quits
    // to save any preferences that haven't yet been saved by
    // our idle function and shut down our connection to IC.
{
    OSStatus junk;
    
    if ( gICInstance != nil ) {
    
        if ( gTicksOfLastPrefChange != kPrefsCleanTicks ) {
            SavePreferences();
        }
    
        junk = ICStop(gICInstance);
        OTAssert("TermPreferences: Error closing IC", junk == noErr);
        gICInstance = nil;
    }
}
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** Startup and Shutdown Code *****
 
static void InitMacToolbox(void)
    // Init all standard Mac OS managers.
    // How many times have I written this?
{
    InitGraf(&qd.thePort);
    InitFonts();
    InitWindows();
    InitMenus();
    TEInit();
    InitDialogs(nil);
    MaxApplZone();
    InitCursor();
}
 
// InitOpenTransportWithMemoryLimit Big Picture
// --------------------------------------------
//
// The LogEngine uses the OT memory allocation routines
// (OTAllocMem, OTFreeMem) to allocate space for log entries in
// the notifier.  This memory comes from the OT client memory pool
// that OT creates for us when we call InitOpenTransport.  However,
// this pool has some bad characteristics:
//
//  1.  The pool starts off very small, and only grows when we allocate
//      memory from it.  As we do all our allocation from our notifier
//      (which is interrupt time with respect to the system Memory Manager)
//      the pool can't grow immediately.  So the pool will often run
//      be full (ie OTAllocMem will return nil) even though the application
//      has plenty of memory.  The pool will later grow, but we've already
//      dropped the log entry on the floor.
//
//  2.  Because the pool starts off small and grows by pieces, we get
//      an extremely fragmented pool.  While this works, its definitely
//      sub-optimal.
//
//  3.  If we're being hammered by strlog (ie people are calling strlog a
//      lot), the pool will keep growing and there's nothing to stop
//      the pool consuming our entire application heap.  When it does so,
//      various toolbox routines (eg QuickDraw) fail ungracefully, ie
//      SysError(25).
//
// See DTS Technote 1128 "Understanding OT Memory Management" for more 
// details on OT client pools.
//
//     <http://developer.apple.com/technotes/tn/tn1128.html>
//
// My old solution to this was to call ASLM to pre-create our client pool 
// in a dedicated zone.  That way the pool could grow without running out 
// heap out of memory.  ASLM allows you to control the initial size 
// of the pool, so I created a pool with a large initial size to avoid 
// the other problems described above.
//
// This solution had one big problem: it relies on ASLM.  ASLM is going 
// away (and has been for many years).  Taking a dependency on ASLM is 
// a bad thing.  Also, relying on the fact that OT uses ASLM as its 
// underlying client pool mechanism is a bad idea because, even if ASLM 
// doesnÕt go away, thereÕs no guarantee that OT will use it.  In fact, 
// a impending release of OT will not use ASLM at all, and thus will 
// implement its own client pool mechanism independent of the ASLM client 
// pool.
// 
// This alternative solution is based on the concept of an OT memory 
// reserve.  When we start the application we create a memory reserve 
// by allocating memory from the OT client pool.  When we need memory we 
// try to get it from the client pool and, if that fails, free some our 
// memory reserve and try again.
//
// See "OTMemoryReserve.h" for more conceptual details about the OT memory 
// reserve module and how the following parameters fit into the scheme of 
// things.
 
enum {
    kBytesReservedForToolboxInApplicationZone = 100L * 1024L,
        // This value represents the minimum number of 
        // bytes that should remain in the application heap after
        // we've allocated the OT memory reserve.
    
    kOTMemoryReserveChunkSize = 128L * 1024L,
        // The chunk size to use when creating the OT memory reserve.
    
    kOTMemoryReserveMinChunks = 6,
        // The minimum number of chunks that we need to obtain if 
        // weÕre going to run.  This roughly corresponds to our 
        // minimum application partition of 1 MB.
    
    kOTMemoryReserveMaxChunks = 100
        // The maximum number of chunks that we need for our memory 
        // pool.  This multiplied by our chunk size comes out around 
        // 12 MB.  If we have more than 12 MB of strlog messages 
        // lying around, something bad has happened.
};
 
static OSStatus InitOpenTransportWithMemoryLimit(void)
{
    OSStatus err;
    OTClientContextPtr junkClientContext;
    
    err = InitOpenTransportInContext(kInitOTForApplicationMask, &junkClientContext);
    if (err == noErr) {
        err = InitOTMemoryReserve(kBytesReservedForToolboxInApplicationZone, kOTMemoryReserveChunkSize, 
                                           kOTMemoryReserveMinChunks, kOTMemoryReserveMaxChunks,
                                           nil);
        if (err != noErr) {
            CloseOpenTransportInContext(nil);
        }
    }
    return err;
}
    
static OSStatus InitApplication(void)
    // Initialises the application-specific stuff.
{
    OSStatus err;
    OSStatus junk;
    Handle mbarH;
    MenuHandle menuH;
    Rect viewRect;
    Rect dataBounds;
    Cell cellSize;
    
    // Setup the menu bar.
    
    mbarH = GetNewMBar(rMenuBar);
    OTAssert("InitApplication: Could not get menu bar", mbarH != nil);
    SetMenuBar(mbarH);
    InvalMenuBar();
    
    // Setup the Apple menu.
    
    menuH = GetMenuHandle(mApple);
    OTAssert("InitApplication: Could not get Apple menu", menuH != nil);
    AppendResMenu(menuH, 'DRVR');
    
    // Install our AppleEvent handlers.
    
    junk = AEInstallEventHandler(kCoreEventClass, kAEOpenApplication, NewAEEventHandlerProc(AEOpenApplicationHandler), 0, false);
    OTAssert("InitApplication: Could not install event handler", junk == noErr);
    junk = AEInstallEventHandler(kCoreEventClass, kAEOpenDocuments, NewAEEventHandlerProc(AEOpenDocumentsHandler), 0, false);
    OTAssert("InitApplication: Could not install event handler", junk == noErr);
    junk = AEInstallEventHandler(kCoreEventClass, kAEQuitApplication, NewAEEventHandlerProc(AEQuitApplicationHandler), 0, false);
    OTAssert("InitApplication: Could not install event handler", junk == noErr);
 
    // Create the main window.
        
    gMainWindow = GetNewDialog(rMainDialog, nil, (WindowPtr) -1L);
    OTAssert("InitApplication: Could not create main window", gMainWindow != nil);
 
    gMainWindowGrowBounds.top = gMainWindow->portRect.bottom;
    gMainWindowGrowBounds.left = gMainWindow->portRect.right;
    gMainWindowGrowBounds.bottom = qd.screenBits.bounds.bottom;
    gMainWindowGrowBounds.right = qd.screenBits.bounds.right;
    
    // And the list that's in the main window.
    
    gListDefProcUPP = NewListDefProc(ListDefProc);
    OTAssert("InitApplication: Could not create UPP for ListDefProc", gListDefProcUPP != nil);
    
    GetDialogItemRect(gMainWindow, ditLogEntryList, &viewRect);
    viewRect.bottom -= 15;
    viewRect.right -= 15;
    SetRect(&dataBounds, 0, 0, 1, 0);
    // SetPt(&cellSize, viewRect.right - viewRect.left, 16);
    SetPt(&cellSize, 1000, 16);
    gLogList = LNew(&viewRect,          // rView
                    &dataBounds,        // dataBounds
                    cellSize,           // cSize
                    rSimpleLDEF,        // theProc
                    gMainWindow,        // theWindow
                    true,               // drawIt
                    true,               // hasGrow
                    true,               // scrollHoriz
                    true);              // scrollVert
    OTAssert("InitApplication: Could not create ListHandle", gLogList != nil);
    (**gLogList).refCon = (long) gListDefProcUPP;
    
    gListUserItemUpdateUPP = NewUserItemProc(ListUserItemUpdateProc);
    OTAssert("InitApplication: Could not create UPP for ListUserItemUpdateProc", gListUserItemUpdateUPP != nil);
 
    SetDialogItemUserItemProc(gMainWindow, ditLogEntryList, gListUserItemUpdateUPP);
    
    // Restore our saved preferences, or setup defaults if
    // we have none.
    
    RestorePreferences();
    
    // Sync the Start and Stop buttons with the state of
    // the back end.
    
    UpdateStartStopUI();
    
    // Startup Open Transport.  We have to do some special
    // things to prevent OT eating our entire heap.  See
    // the comments associated with this routine.
    
    err = InitOpenTransportWithMemoryLimit();
 
    return err;
}
 
static void TermApplication(void)
    // Shut down our application.  This is normal quitting,
    // ie via AppleEvent or via menu command.  The main event
    // loop has already shut down the logging stuff.  All we
    // need to do is close down the connection to Open Transport,
    // ASLM and Internet Config.
    //
    // Note that we make no attempt to clean up our toolbox stuff
    // or memory allocates.  The Process Manager will do that for us
    // and there's no point writing extra code (and extra bugs) to
    // duplicate that here.
{
    TermOTMemoryReserve();
    CloseOpenTransportInContext(nil);
    TermPreferences();
    
    OTAssert("TermApplication: Quitting with raw streams open!!!", ! LoggingActive() );
}
 
// Raw streams are not tracked by Open Transport.  When the
// application quits abnormally, we have to make sure that
// we have closed the stream, otherwise it will dangle around
// in memory. This would not be too bad except:
//
// a) the notifier may be called, calling code that is no
//    no longer loaded, and
// b) the log driver won't let another client hook up as
//    the stream logger as long as the stream is open.
//
// So we have a CFM terminate procedure that shuts down
// the raw stream if it's open.
//
// Note that I don't recommend this approach for closing
// endpoints and such.  OT has automatic shutdown code,
// and I recommend that you rely on it for emergency shutdown
// in the general case.  This code is only necessary because
// we're using raw streams.
//
// Of course, for normal shutdown, ie the user chooses Quit, you
// should not rely on OT's emergency shutdown support; you
// should always call CloseOpenTransport yourself for normal
// shutdown.
 
extern pascal void EmergencyShutdown(void);
 
extern pascal void EmergencyShutdown(void)
{
    if ( LoggingActive() ) {
        OTDebugStr("EmergencyShutdown: Had to StopLogging");
        StopLogging();
    }
}
 
/////////////////////////////////////////////////////////////////////
#pragma mark **** The Main Line! *****
 
extern void main(void)
{
    OSStatus err;
 
    InitMacToolbox();
    err = InitApplication();
    OTAssert("main: InitApplication returned an error", err == noErr);
    if (err == noErr) {
    
        #if MORE_DEBUG
            if (true) {
                OTMemoryReserveTest();
            }
        #endif
    
        MainEventLoop();
        
        TermApplication();
    }
}