Just got another one.

You will see, in the code I've included below, that I am almost to the point of just eating and ignoring this error, because it makes absolutely no sense that it occurs. But such a strategy seems to me to be dangerous and foolhardy, so:

At the risk of thoroughly embarrassing myself, I am going to lay out all of the details on this in the hope that some kind soul will take an interest. I realize this is a big request. There's a lot here to look at. Feel free to ignore me. Thanks VERY MUCH if you don't.

Below is the entire stack trace from the error. I have omitted nothing. I am adding commentary and quite a bit of code to illustrate exactly what is going on.

A few configuration points:

SET EXCLUSIVE is OFF
SET MULTILOCKS is ON

I use a dbc with these tables, which is opened after those two commands are issued, and therefore should implicitly be open SHARED.

I DO NOT use private datasessions. Everything occurs in the same, default VFP datasession.

I do not use any form of buffering.
I do not use views.
I do not use data binding of any kind.

Here is the trace, with explanatory comments:

** Preliminary error information:
A DATA VALIDATION ERROR!
Called from: Line No. 0 in MaintenanceMgr.ValidateTables:

DATA VALIDATION ERROR!

The following error occurred
while attempting to open the table:

J:\cildata2\clpeople.dbf

'VFP Error No. 3: File is in use.'

The program will abort.

Shared Data Path: J:\cildata2\
Directory J:\cildata2\ exists.

CURDIR(): \CIL DATA 2\

Level: 1 Program File: c:\cil data 2\cildata2.exe Module/Object: cilmain Source File: C:\CIL Data 2\cilmain.prg Line Number: 744 && Main Program Level: 2 Program File: c:\stic foxpro framework\base classes\managers.vct Module/Object: cilapp.mainloop Source File: c:\stic foxpro framework\base classes\managers.vct Line Number: 153 Line Contents: READ EVENTS && Application Manager Level: 3 Program File: c:\cil data 2\personform.sct Module/Object: personform.cmdgo.click Source File: c:\cil data 2\personform.sct Line Number: 5 Line Contents: THISFORM.Search() Level: 4 Program File: c:\cil data 2\personform.sct Module/Object: personform.search Source File: c:\cil data 2\personform.sct Line Number: 208 Line Contents: theresult = THISFORM.oBusPeoBusiness.Search(THIS,theterm,thetype,thefield,thedeptable, skipfilter,usedistinct)

The user is searching for somebody's name in order to display a record.

Level: 5 Program File: cilbusobjs.fxp Module/Object: personform.obuspeobusiness.search Source File: C:\CIL Data 2\cilbusobjs.prg Line Number: 5639 Level: 6 Program File: searchman.fxp Module/Object: personform.obuspeobusiness.osearch.search Source File: C:\CIL Data 2\searchman.prg Line Number: 375 Level: 7 Program File: basedataclasses.fxp Module/Object: personform.obuspeobusiness.osearch.oqueries.doquery Source File: C:\stic foxpro framework\base classes\basedataclasses.prg Line Number: 9293 Level: 8 Program File: basedataclasses.fxp Module/Object: personform.obuspeobusiness.osearch.oqueries.executequery Source File: C:\stic foxpro framework\base classes\basedataclasses.prg Line Number: 9693

This code runs the assembled SQL SELECT query expression as a macro, ie;:

&QueryToRun

When the query is finished in a PersonForm.Search(), either a MESSAGEBOX() will appear announcing "No matches found" and offering the opportunity to start a new record, or a pick list window will appear showing matching records.

As you can see, the trace does not show that either of these things happened.

If there's an error in the query, the Query Manager will display it and retreat to a safe place. Query errors do not trigger a program shutdown and are not logged. The Query Manager cleans up after itself; nothing it does alters anything in the general environment.

Level: 9 Program File: c:\stic foxpro framework\base classes\basicforms.vct Module/Object: mainwindow.queryunload Source File: c:\stic foxpro framework\base classes\basicforms.vct Line Number: 37 Line Contents: IF oApp.CloseDown() = .T.

Main window is being closed.

As I reported previously, this would have to be the result of manual action by the user at this point.

oApp.CloseDown() poses an "Are you sure?" MESSAGEBOX(). If the user confirms, the code asks the main window to make a label visible that says "Please Wait..." Also, the main window's Close button is disabled so an impatient user can't press it twice.

Then oApp.SaveSystemSettings() executes. If the user is not a System Administrator, that method returns without doing anything.

In the case of this error, the user is not a System Administrator.

Next, oApp.CloseDown() calls Downshut

Level: 10 Program File: c:\stic foxpro framework\base classes\managers.vct Module/Object: cilapp.closedown Source File: c:\stic foxpro framework\base classes\managers.vct Line Number: 52 Line Contents: DO Downshut Level: 11 Program File: endroutines.fxp Module/Object: downshut Source File: C:\CIL Data 2\endroutines.prg Line Number: 1913

At this point the code carries out the following steps, each of which must be successful, without errors, and RETURN .T. before the next one can be carried out:

Close all windows except the main window
Log the current user out
Find out how many users are still logged in, and if that number is 0, create a semaphore file in the shared data location that will prevent any other users from logging in

If there are no logged in users and the semaphore file exists, then the code instanciates a maintenance object, which proceeds to test all of the shared data tables to make sure they haven't been corrupted (by trying to open them), before proceeding to the next step, which would be to back them up to another location.

That's where we are now.

Level: 12 Program File: endroutines.fxp Module/Object: routinebackup Source File: C:\CIL Data 2\endroutines.prg Line Number: 1532 Level: 13 Program File: basedataclasses.fxp Module/Object: stcustom.validatetables Source File: C:\stic foxpro framework\base classes\basedataclasses.prg Line Number: 11690

Below is the entire ValidateTables() method, exclusive of variable initializations, which, I assure you, are all there. There is nothing undefined here. BTW, this method also runs at program start-up, and it was successful at that time or the user wouldn't have gotten to run a query in the PersonForm window.

Sometimes when this bizarro Error 3 occurs I will see THIS.ValidateDictionary(), which is called in the method below, in the stack trace. In this particular case, and the last couple of similar cases, it hasn't been there. However, ValidateDictionary() and the other methods that get called are also below.

Call them stupid, call them unnecessary, they have been in my framework for 8 years and they work just fine at least 98% of the time.

However, the number of concurrent users of the program has been increasing rather rapidly over the past couple of years.

The framework was developed in VFP 7, before there was TRY...CATCH. The error-handler swapping code I used worked fine then, and still should. You will also see comments discussing various possible errors that can occur in the code due to circumstances beyond my control. Those comments assume that VFP will generate an error number appropriate to the situation, although I now hypothesize that it doesn't always do that.

In particular, you will see that none of these methods appear in the stack trace in this situation. However, at some point, it seems to me that they must have run, because the ultimate error is VFP Error 3 ("File is in use"), which, in this context, could only happen on a USE command issued on a table that is already USED() in the current work area (the other options are RENAME or DELETE, neither of which appear anywhere in the code). I don't ever assign substitute ALIASes when I open tables, and I don't ever use USE AGAIN. So the way my code is structured, that should not be able to happen. As far as I can tell. But obviously one of my assumptions is wrong.

PROCEDURE ValidateTables
          * 1. Get a path for the application data.
          IF TYPE("oApp") = "O" AND NOT ISNULL(oApp)
               thedatadictionary = oApp.DataDictionary
          ELSE
* Use the PRIVATE Project-Specific Setting variable directly from the main
               * program.
               thedatadictionary = DataDictName
          ENDIF

          IF EMPTY(THIS.SharePath)
               IF TYPE("oEnv") = "O" AND NOT ISNULL(oEnv)
                    THIS.SharePath = oEnv.SharedDataPath
               ELSE
                    THIS.SharePath = THIS.PullPathData()

                    THIS.CurrentProcedure = "MaintenanceMgr.ValidateTables"
               ENDIF

               IF EMPTY(THIS.SharePath)
                    errmess = "No application data path available."

                    oktocontinue = .F.
               ENDIF
          ENDIF

          * 2. Make sure the data dictionary is there.
          IF oktocontinue = .T.
               apppath = ADDBS(ALLTRIM(THIS.SharePath))

               * Hook method.
THIS.BeforeValidateTables() && This just RETURNS .T.; there's no subclass code

               THIS.CurrentProcedure = "MaintenanceMgr.ValidateTables"

* Open the data dictionary table if it exists; will return .F. if any aspect of
               * that table is missing or unusable.
oktocontinue = THIS.ValidateDictionary(thedatadictionary,apppath)

               THIS.CurrentProcedure = "MaintenanceMgr.ValidateTables"

               IF oktocontinue = .F.
* ValidateDictionary() will have placed an error message in
                    * TableProcessingError.
                    errmess = THIS.TableProcessingError
               ENDIF
          ENDIF

          * 3. Run tests on every data file listed in the data dictionary.
          IF oktocontinue = .T.
* Get information on add-in modules, so we can determine whether to ignore Data * Dictionary entries for tables that are associated with those modules.

* The presence of the configx.dbf table in the Shared Data Path indicates that
               * we can run module-related code safely.
               IF FILE((apppath + "configx.dbf"))
* Instanciate the Modules Utilities object. (Each call to ModuleInUse() * will do this if it doesn't already exist, but it's better to trap the
                    * error here.)
                    oktocontinue = THIS.ListModules()

                    THIS.CurrentProcedure = "MaintenanceMgr.ValidateTables"

                    IF oktocontinue = .F.
                         errmess = "Could not retrieve module information."
                    ENDIF
               ENDIF
          ENDIF

          olderror = ON("ERROR")

          IF oktocontinue = .T.
scanfor = '(NOT DELETED("' + thedatadictionary + '")) AND NOT EMPTY(dicttable)'

oktocontinue = THIS.ProcessDictionaryTables(thedatadictionary,"V",apppath, ;
                                                           scanfor,"",0)

THIS.CurrentProcedure = "MaintenanceMgr.ValidateTables"

               IF NOT EMPTY(olderror)
                    ON ERROR &olderror
               ENDIF
          ENDIF

* NOTE: ShutTable() only returns .F. if we didn't pass it a table name.
          THIS.ShutTable((thedatadictionary))

          THIS.CurrentProcedure = "MaintenanceMgr.ValidateTables"

          IF NOT EMPTY(errmess)
* An error occurred in this method before we got to the ProcessDictionaryTables()
               * method. Display it if appropriate.
errmess = "DATA VALIDATION ERROR!" + CHR(13) + CHR(13) + errmess ;
                       + CHR(13) + CHR(13) ;
+ "If you have email error notifications enabled," + CHR(13) ; + "the program will try to email the programmer" + CHR(13) ;
                       + "about this." + CHR(13) + CHR(13) ;
                       + "Then the program will abort."
          ELSE
               IF NOT EMPTY(THIS.TableProcessingError)
* An error occurred in ProcessDictionaryTables(). That method displayed the
                    * error but we still have to log it.
                    errmess = THIS.TableProcessingError
               ENDIF
          ENDIF

          IF NOT EMPTY(errmess)
               oMsgObj = CREATEOBJECT("MsgObj")

               IF TYPE("oMsgObj") = "O" AND NOT ISNULL(oMsgObj)
                    oMsgObj.MsgType = "ERROR"
                    oMsgObj.MsgText = errmess
                    oMsgObj.TimeOutValue = 30000
                    oMsgObj.MsgCaption = "DATA VALIDATION ERROR!"
                    oMsgObj.ErrorName = "DATA VALIDATION ERROR!"
                    oMsgObj.LineNumber = 0
                    oMsgObj.ProgramName = THIS.CurrentProcedure
                    oMsgObj.CodeLine = ""
                    oMsgObj.ErrorMsg = errmess
               ENDIF

               THIS.MessageCentral(oMsgObj)
          ELSE
               * No problems detected!
          ENDIF

          * Hook method.
          THIS.AfterValidateTables()

          THIS.CurrentProcedure = "MaintenanceMgr.ValidateTables"

          RETURN oktocontinue
ENDPROC

     PROTECTED PROCEDURE ValidateDictionary
          * Parameters:
          *
          * cName       - Name of the data dictionary table. REQUIRED.
* cPath - Fully qualified path for the data dictionary table. REQUIRED. * lCloseAfter - Flag; controls whether we attempt to close the data dictionary * table after completing all tests. OPTIONAL; if empty or omitted,
          *               we leave it open.
          LPARAMETERS cName, cPath, lCloseAfter

          THIS.CurrentProcedure = "MaintenanceMgr.ValidateDictionary"

          LOCAL oktocontinue, thedictname, thedictpath, errmess, curfile

okcontinue = .T. && Success/continuation flag/return value; MUST start as .T.
          thedictname = ""  && Name of the data dictionary table
          thedictpath = ""  && Path to the data dictionary table
          errmess = ""      && Error message to pass on, if any
          curfile = ""      && Fully-qualified path and file name

          THIS.TableProcessingError = ""

          DO CASE
               CASE EMPTY(cName)
                    errmess = "No Data Dictionary Name."

               CASE EMPTY(cPath)
                    errmess = "No Data Dictionary Path."

          ENDCASE

          oktocontinue = EMPTY(errmess)

          IF oktocontinue = .T.
               thedictname = cName

               thedictpath = cPath

               * Consolidate file checks:
               DO CASE
                    CASE NOT FILE((thedictpath) + thedictname + ".dbf")
                         errmess = "The data dictionary table can't be found."

                    CASE NOT FILE((thedictpath) + thedictname + ".cdx")
errmess = "The data dictionary index file can't be found."

               ENDCASE

               oktocontinue = EMPTY(errmess)
          ENDIF

          IF oktocontinue = .T.
               curfile = thedictpath + thedictname + ".dbf"

               oktocontinue = THIS.OpenTable((curfile))

               THIS.CurrentProcedure = "MaintenanceMgr.ValidateDictionary"

               IF oktocontinue = .F.
                    errmess = "Could not open data dictionary table."
               ENDIF
          ENDIF

          IF oktocontinue = .T.
               oktocontinue = THIS.TableHasData((thedictname))

               THIS.CurrentProcedure = "MaintenanceMgr.ValidateDictionary"

               IF oktocontinue = .F.
                    errmess = "The data dictionary table is empty."
               ENDIF
          ENDIF

          IF lCloseAfter = .T. AND NOT EMPTY(thedictname)
               THIS.ShutTable(thedictname)

               THIS.CurrentProcedure = "MaintenanceMgr.ValidateDictionary"
          ENDIF

          IF NOT EMPTY(errmess)
* Put the error message into a property and let the calling method handle it. THIS.TableProcessingError = "DICTIONARY VALIDATION ERROR:" + CHR(13) + CHR(13) ;
                                         + errmess
          ENDIF

          RETURN oktocontinue
     ENDPROC

    PROTECTED PROCEDURE OpenTable
          ** This method is a wrapper for the VFP USE command.
          **
          ** RETURNS: .T. if successful, .F. if not.

          * Parameters:
          *
          * cTableName - Name of the table to open. REQUIRED.
          LPARAMETERS cTableName

          THIS.CurrentProcedure = "MaintenanceMgr.OpenTable"

          IF EMPTY(cTableName)
               RETURN .F.
          ENDIF

          IF THIS.FileTestMode = .T.
* This error handler has not been used so far in testing, because this * object's Error() method preempts it, but it's here just in case...
               ON ERROR RETURN ERROR()
          ENDIF

          IF NOT THIS.TableIsOpen( (JUSTSTEM(cTableName)) )
               THIS.CurrentProcedure = "MaintenanceMgr.OpenTable"

               SELECT 0

               USE (cTableName) ALIAS (JUSTSTEM(cTableName))
          ELSE
               THIS.CurrentProcedure = "MaintenanceMgr.OpenTable"

               SELECT (JUSTSTEM(cTableName))
          ENDIF

          THIS.CurrentProcedure = "MaintenanceMgr.OpenTable"

* In File Test Mode, the attempt to open the table may generate a VFP error. In that * case, this object's Error() method will catch it, put message text into the * FileTestError property, and return to this method. We need to pass that message
          * back to ValidateTables() in that case.
          IF THIS.FileTestMode = .T.
               THIS.FileTestMode = .F.

               IF NOT EMPTY(THIS.FileTestError)
                    RETURN THIS.FileTestError
               ELSE
                    RETURN .T.
               ENDIF
          ELSE
               RETURN .T.
          ENDIF
     ENDPROC

     PROTECTED PROCEDURE TableIsOpen
** This method is a wrapper for the VFP USED() function. It provides a standard ** interface for determining whether a particular table is available for access. ** Assuming such a test is necessary in a non-xBase situation, a subclass of this ** class could put completely different code here and we wouldn't have to change
          ** any other code that calls for such a test.
          **
          ** RETURNS: .T. if the table is open, .F. if it isn't.

          * Parameters:
          *
* cAlias - The alias of the work area being tested; for best results, this should be
          * the same as the table name.
          LPARAMETERS cAlias

          THIS.CurrentProcedure = "MaintenanceMgr.TableIsOpen"

          LOCAL thereturn

          thereturn = .F.

          IF USED( (cAlias) )
               thereturn = .T.
          ELSE
               thereturn = .F.
          ENDIF

          RETURN thereturn
     ENDPROC

     PROTECTED PROCEDURE TableHasData
          ** This method tests to see if an open table contains any records.
          **
          ** NOTE: This interface is the same as that for the
** RETURNS: .T. if the table has countable records, .F. if it doesn't.

          * Parameters:
          *
* cTableName - Table to check; may be a JUSTSTEM() alias or a full path and file
          *                   name (with the "dbf" extension). REQUIRED.
* lIncludeDeleted - Flag that indicates if deleted records are included in the count; * if .F., a table that contains only deleted records will be deemed
          *                   empty. OPTIONAL.
          LPARAMETERS cTableName, lIncludeDeleted

          THIS.CurrentProcedure = "MaintenanceMgr.TableHasData"

          LOCAL therecord, thecount, delcount, withdel, thereturn

          therecord = 0    && RECNO() of the current pointer position
          thecount = 0     && Number of records in the table (from RECCOUNT())
          delcount = 0     && Number of deleted records in the table
withdel = .F. && Flag to indicate whether to count deleted records
          thereturn = .F.  && Return value

          * Validation.
          IF EMPTY(cTableName)
               RETURN .F.
          ENDIF

* Dynamic write-able paths means we may receive a fully-qualified path and file name
          * here instead of an alias.
          IF THIS.TableIsOpen( (JUSTSTEM(cTableName)) ) = .F.
               RETURN .F.
          ENDIF

          THIS.CurrentProcedure = "MaintenanceMgr.TableHasData"

          withdel = lIncludeDeleted

          therecord = RECNO()

* Dynamic write-able paths means we may receive a fully-qualified path and file name
          * here instead of an alias.
          thecount = RECCOUNT((JUSTSTEM(cTableName)))

          COUNT FOR DELETED() TO delcount

          DO CASE
               CASE thecount < 1
                    thereturn = .F.

               CASE thecount - delcount < 1 AND withdel = .F.
                    thereturn = .F.

               OTHERWISE
* I guess RECCOUNT() includes the "phantom record" if there are any real
                    * records in the table?
                    IF (NOT EMPTY(therecord)) AND (therecord <= thecount)
                         GO therecord
                    ENDIF

                    thereturn = .T.
          ENDCASE

          RETURN thereturn
     ENDPROC

     PROTECTED PROCEDURE ShutTable
          ** This method attempts to close an open table.
          **
          ** RETURNS: .T. if the table was closed or wasn't open; .F. if an
          **          error occurs.

          * Parameters:
          *
          * cTableName - Table to close. REQUIRED.
          LPARAMETERS cTableName

          THIS.CurrentProcedure = "MaintenanceMgr.ShutTable"

          IF EMPTY(cTableName)
               RETURN .F.
          ENDIF

          IF NOT THIS.TableIsOpen((cTableName))
               RETURN .T.
          ELSE
               THIS.CurrentProcedure = "MaintenanceMgr.ShutTable"

               SELECT (cTableName)

               USE IN (cTableName)

               RETURN .T.
          ENDIF
     ENDPROC


     PROTECTED PROCEDURE ProcessDictionaryTables
** This method loops through tables and related files listed in the specified data
          ** dictionary, and performs a requested action on them.
          **
** ASSUMES: The specified data dictionary table is open in the current work area.
          **
          ** RETURNS: .F. if an error occurs; .T. otherwise.

          * Parameters:
          *
* cDataDictionary - Name of the data dictionary table that has the list of tables
          *                   to process. REQUIRED.
* cAction - Code for the action to take: "V" - Validate; "B" - Backup;
          *                   "P" - Pack. REQUIRED.
* cPath - Location of the tables to be processed, whether Shared or Local.
          *                   REQUIRED.
* cScanFor - Logical VFP XBase filter expression for a SCAN command. REQUIRED. * cToLocation - Full path location to copy files to, if backing up. REQUIRED for
          *                   a Backup action; ignored for other actions.
* nTableCount - Number of tables to process, for use with the Progress display. * OPTIONAL; if empty or omitted, the Progress display is not
          *                   updated.
LPARAMETERS cDataDictionary, cAction, cPath, cScanFor, cToLocation, nTableCount

          THIS.CurrentProcedure = "MaintenanceMgr.ProcessDictionaryTables"

LOCAL errmess, oktocontinue, thedatadictionary, theaction, apppath, scanfor LOCAL thelocation, tablecount, thisfile, progcount, themod, filepath, curfile
          LOCAL olderror, opentest, errheader, errcloser

errmess = "" && Specific content for an error message oktocontinue = .T. && Success/continuation flag/return value; MUST start as .T. thedatadictionary = "" && Name of the data dictionary table (without the ".dbf")
          theaction = ""          && Code for the action to take
apppath = "" && Data path for application tables, either local or shared scanfor = "" && Logical SCAN FOR expression to limit the tables processed
          thelocation = ""        && Path to backup data to
tablecount = 0 && Number of tables to process, for the progress display
          thisfile = ""           && Data file name without an extension
progcount = 0 && Running count of tables processed, for progress display themod = "" && Code for an add-on module that requires authorization
          filepath = ""           && Path to check for a particular file
          curfile = ""            && A full path, file name, and extension
olderror = "" && Previous setting of ON ERROR for validation tests opentest = .F. && Return from THIS.OpenTable() for a validation test
          errheader = ""          && "Header" line for an error message
          errcloser = ""          && Final line for an error message

          THIS.TableProcessingError = ""

          DO CASE
               CASE EMPTY(cDataDictionary)
                    errmess = "No Data Dictionary."

               CASE EMPTY(cAction)
                    errmess = "No Action."

               CASE EMPTY(cPath)
                    errmess = "No Path."

               CASE EMPTY(cScanFor)
                    errmess = "No Scan For."

               CASE cAction == "B" AND EMPTY(cToLocation)
                    errmess = "No Backup Location."

          ENDCASE

          oktocontinue = EMPTY(errmess)

          IF oktocontinue = .T.
               thedatadictionary = cDataDictionary

               theaction = cAction

               apppath = cPath

               scanfor = cScanFor

               thelocation = cToLocation

               IF TYPE("nTableCount") = "N"
                    tablecount = nTableCount
               ENDIF

               SCAN FOR &scanfor.
                    thisfile = ALLTRIM(dicttable)

                    IF theaction == "P"
* If we don't fail on the SCAN FOR... command, then update the public * variable with the name of the current table we're looking at. PackScanForExp = scanfor + " Handling table: " + thisfile
                    ENDIF

                    * We can't do this one while it's open.
                    IF UPPER(thisfile) == UPPER(thedatadictionary)
                         LOOP
                    ENDIF

* There's already a record in the Data Dictionary for the configx.dbf table, * which stores information about add-in modules. However, if the table isn't * there, this isn't an error; it means that we aren't yet ready to run code * related to add-in modules. So we'll just skip it.
                    IF UPPER(thisfile) == "CONFIGX"
                         IF NOT FILE((apppath + "configx.dbf"))
                              progcount = progcount + 1

                              LOOP
                         ENDIF
                    ENDIF

* If the table is part of an add-in module that's not authorized for use,
                    * skip it.
                    *
* The presence of the configx.dbf table in the Shared Data Path indicates
                    * that we can can run module-related code safely.
                    IF FILE((apppath + "configx.dbf"))
* Get the authorization code from the Data Dictionary.
                         themod = modcode

                         * See if it's authorized.
                         IF THIS.ModuleInUse(themod) = .F.
* ModuleInUse() uses an array of modules that is set up by calling * this class's ListModules() method before we run this code. THIS.CurrentProcedure = "MaintenanceMgr.ProcessDictionaryTables"

                             progcount = progcount + 1

                             LOOP
                         ENDIF

THIS.CurrentProcedure = "MaintenanceMgr.ProcessDictionaryTables"
                    ENDIF

* If a file location path is specified in the data dictionary, use it; * otherwise, use the application data path for system-wide files.
                    IF EMPTY(fileloc)
                         DO CASE
                              CASE availto == "S"
                                   filepath = apppath

                              CASE availto == "L"
                                   * Write-able locations are dynamic.
                                   IF TYPE("oEnv") = "O" AND NOT ISNULL(oEnv)
filepath = oEnv.ObtainLocalFilePaths("locprefs.dbf")

                                        filepath = ADDBS(JUSTPATH(filepath))
                                   ELSE
                                        filepath = ""
                                   ENDIF

                              OTHERWISE
errmess = "No availto for " + thisfile + "."

                                   oktocontinue = .F.

                                   EXIT
                         ENDCASE
                    ELSE
                         * Use the specified file location.
                         filepath = ADDBS(ALLTRIM(fileloc))
                    ENDIF

* See if the table (dbf) file is there; if it is, we can process it. If not,
                    * throw an error.
                    curfile = filepath + thisfile + ".dbf"

                    IF NOT FILE((curfile))
                         errmess = "Can't find " + thisfile + " table."

                         oktocontinue = .F.

                         EXIT
                    ENDIF

* If the table should have a .cdx file, and it's there, we can process it;
                    * if not, throw an error.
                    IF hascdx = .T.
                         curfile = filepath + thisfile + ".cdx"

                         IF NOT FILE((curfile))
                              errmess = "Can't find " + thisfile + " index."

                              oktocontinue = .F.

                              EXIT
                         ENDIF
                    ENDIF

* If the table should have a .fpt (memo) file, and it's there, we can process
                    * it; if not, throw an error.
                    IF hasfpt = .T.
                         curfile = filepath + thisfile + ".fpt"

                         IF NOT FILE((curfile))
errmess = "Can't find " + thisfile + " memo file."

                              oktocontinue = .F.

                              EXIT
                         ENDIF
                    ENDIF

                    * We always start with the table file.
                    curfile = filepath + thisfile + ".dbf"

                    DO CASE
                         * Backup.
                         CASE theaction == "B"
COPY FILE (curfile) TO (thelocation) + thisfile + ".dbf"

                              IF hascdx = .T.
curfile = filepath + thisfile + ".cdx"

COPY FILE (curfile) TO (thelocation) + thisfile + ".cdx"
                              ENDIF

                              IF hasfpt = .T.
                                   curfile = filepath + thisfile + ".fpt"

COPY FILE (curfile) TO (thelocation) + thisfile + ".fpt"
                              ENDIF

                         * Validate.
                         CASE theaction == "V"
* Try to open the table. This may generate a VFP error that we have
                              * to trap.
                              olderror = ON("ERROR")

* This flag causes OpenTable() to turn off the current error handler. * If a VFP error occurs, this flag also causes OpenTable() to return * an error message string instead of a logical value.
                              THIS.FileTestMode = .T.

                              opentest = THIS.OpenTable(curfile)

THIS.CurrentProcedure = "MaintenanceMgr.ProcessDictionaryTables"

* If OpenTable() returned a logical variable, there may still have
                              * been a problem.
                              IF TYPE("opentest") = "L"
                                   IF opentest = .F.
errmess = "Could not open table " + thisfile + "."

                                        oktocontinue = .F.

                                        EXIT
                                   ELSE
* OpenTable() returned .T. so we can close the table and go * on to the next one. NOTE: ShutTable() only returns .F. if
                                        * we didn't pass it a table name.
                                        THIS.ShutTable((JUSTSTEM(curfile)))

                                        THIS.CurrentProcedure = ;
                                        "MaintenanceMgr.ProcessDictionaryTables"
                                   ENDIF
                              ELSE
                                   IF TYPE("opentest") = "C"
                                        THIS.FileTestError = ""

* There was a VFP error and OpenTable() returned the error
                                        * message.
errmess = "The following error occurred" + CHR(13) ; + "while attempting to open the table:" ;
                                                + CHR(13) + CHR(13) ;
+ curfile + CHR(13) + CHR(13) ;
                                                + "'" + opentest + "'"

                                        oktocontinue = .F.

                                        EXIT
                                   ENDIF
                              ENDIF

                         * Pack.
                         CASE theaction == "P"
                              oktocontinue = THIS.OpenTable(curfile)

THIS.CurrentProcedure = "MaintenanceMgr.ProcessDictionaryTables"

                              IF oktocontinue = .T.
                                   PACK IN (JUSTSTEM(curfile))

                                   THIS.ShutTable((JUSTSTEM(curfile)))

THIS.CurrentProcedure = "MaintenanceMgr.ProcessDictionaryTables"
                              ELSE
errmess = "Couldn't open " + thisfile + " table."

                                   oktocontinue = .F.

                                   EXIT
                              ENDIF
                    ENDCASE

                    IF NOT EMPTY(tablecount)
                         * We're using a progress display, so update it.
                         progcount = progcount + 1

                         THIS.ShowProgress((progcount/tablecount),"")

THIS.CurrentProcedure = "MaintenanceMgr.ProcessDictionaryTables"
                    ENDIF

                    IF theaction == "V"
                         * We're validating.

* IMPORTANT: Our special error detection code is not designed to handle * Error 1104 ("Error reading [file]."), which will occur if the * connection to the Shared Data location is lost. This will be * in play as soon as we get to ENDSCAN, below, because, * apparently, SCAN...ENDSCAN looks at the physical table * location on every iteration even though allegedly the table * is open in a cursor in memory. We have to restore the system * error handler here so it can exit more or less gracefully. * The resulting error log may be confused. It may report that * the error occurred on THIS.OpenTable(), even though it * actually happened as soon as we hit ENDSCAN.
                         IF NOT EMPTY(olderror)
                              ON ERROR &olderror
                         ENDIF
                    ENDIF
               ENDSCAN
          ENDIF

          IF NOT EMPTY(errmess)
               IF NOT EMPTY(theaction)
                    DO CASE
                         CASE theaction == "V"
errheader = "DATA VALIDATION ERROR!" + CHR(13) + CHR(13)

errcloser = CHR(13) + CHR(13) + "The program will abort."

                         CASE theaction == "B"
errheader = "DATA BACKUP ERROR:" + CHR(13) + CHR(13)

errcloser = CHR(13) + CHR(13) + "The backup process will abort."

                         CASE theaction == "P"
errheader = "MaintenanceMgr ERROR:" + CHR(13) + CHR(13)

errcloser = CHR(13) + CHR(13) + "Packing will abort."

                    ENDCASE
               ELSE
                     errheader = "MaintenanceMgr ERROR:" + CHR(13) + CHR(13)

errcloser = CHR(13) + CHR(13) + "The current process will abort."
               ENDIF

               * The calling method may need to log the error.
               THIS.TableProcessingError = errheader + errmess + errcloser

               IF NOT EMPTY(tablecount)
                    * We may have displayed the Progress Window. Close it.
                    THIS.ShowProgress(-1)

THIS.CurrentProcedure = "MaintenanceMgr.ProcessDictionaryTables"
               ENDIF
          ENDIF

          RETURN oktocontinue
     ENDPROC

     PROCEDURE Error
          ** 2/4/15 CHANGED:
          **
** This procedure provides specific error handling for VFP errors that occur during the ** ValidateTables() method's attempt to open a table (to verify that it can be opened). It ** also overrides the baseclass Error() behavior when this object is used during early ** program startup when the other components of the error handling system don't exist.
          **
          ** RETURNS: Nothing

          LPARAMETERS nError, cMethod, nLine, cMessage, cInfo

          LOCAL oldcursor

          oldcursor = ""  && Previous value of SET("CURSOR")

          IF THIS.FileTestMode = .F.
               ** 2/4/15 CHANGED:
** I've seen a rash of instances of Error 1569 on OpenTable() during shutdowns. Can ** this be some kind of contention introduced by my programming activities, which can ** theoretically access the database in EXCLUSIVE mode? I don't know, but the first pass
               ** is to RETRY on that error.
               **
** The errors don't necessarily occur when FileTestMode is .T., and if that's the case, ** I'll allow up to 5 retries over a second of time. (I've also modified the scenario
               ** for FileTestMode = .T., below.)
               IF nError = 1569
                    IF THIS.Error1569Retries < 5
                         THIS.Error1569Retries = THIS.Error1569Retries + 1

                         oldcursor = SET("CURSOR")

                         SET CURSOR OFF

                         WAIT "" TIMEOUT .2

                         SET CURSOR &oldcursor

                         RETRY
                    ENDIF
               ENDIF

* If this object is called after the system has been fully set-up, use the normal
               * error handler.
               IF "FINALSTICERROR" $ UPPER(ON("ERROR"))
                    DODEFAULT(nError,cMethod,nLine,cMessage,cInfo)
               ELSE
* The standard error handler isn't available, so provide an informative but
                    * quick exit.
THIS.MessageCentral("ERROR", "CATASTROPHIC ERROR!" + CHR(13) + CHR(13) ; + "in " + THIS.CurrentProcedure + CHR(13) + CHR(13) ; + "VFP Error No. " + TRANSFORM(nError) + ": " ;
                                        + MESSAGE() + CHR(13) + CHR(13) ;
                                        + "Aborting program.")

                    DO FinalSteps
               ENDIF
          ELSE
* We're in data test mode and an error has occurred while trying to open a table.

               ** 2/4/15 CHANGED (see above also):

* For Error 1569 ("Database [path\filename].dbc: File access denied"), if I, in * my IDE, am the culprit, a simple quick RETRY won't fix it. If it's something * to do with ordinary increased file contention, it may. So I'll try this first.
               *
* For Error 108 ("File is in use by another user"), we just need to wait a bit and * retry. I DON'T want to lower the setting for TABLEVALIDATE here; if there's * something seriously wrong, I want to catch it when we do the data tests.
               *
* For Error 41 ("Memo file 'name' is missing or invalid."). Rationale:
               *
* 1. It's not missing; ValidateTables() specifically tests for that before trying
               *    to open a table that contains Memo fields.
               *
* 2. It's probably not invalid; [name redacted] gets this a lot on the demvals.dbf * table in CIL Data 2, but I've tested that table and neither it nor its .fpt
               *    file is corrupted.
               *
* 3. It appears that this error CAN occur as a result of file contention; see the * the Foxpro Wiki page called "MemoFileMissingOrInvalid".
               IF nError = 108 OR nError = 41 OR nError = 1569
               *IF nError = 108 OR nError = 41
                    oldcursor = SET("CURSOR")

                    SET CURSOR OFF

                    WAIT "" TIMEOUT .1

                    SET CURSOR &oldcursor

                    RETRY
               ELSE
                    ** 4/14/15 ADDED:
                    ** "File in use."
                    **
** According to MS, "You have attempted a USE, DELETE, or RENAME command on a
                    ** file that is currently open."
                    **
** This happened to [name redacted] during a shutdown, on 4/14/15.
                    **
** During Table Validation we do USE [table], but we check for USED(table) ** first. This should only apply to what's going on in the current datasession, ** so whether or not somebody else has the table open shouldn't trigger this ** error. (If it's open exclusively we should get "Access denied"; if it's a ** locking thing, we should get Error 108.) So I don't understand this, and ** I'm just adding this code but not implementing it just yet.
                    *IF nError = 3
* This should cause OpenTable() to return .T. instead of a text error * message, which should result in the error being ignored.
                   *     THIS.FileTestError = ""
                    *ELSE
* For any other error, the program will abort after providing this error
                         * message.
THIS.FileTestError = "VFP Error No. " + TRANSFORM(nError) + ": " + MESSAGE()

                         RETURN
                   *ENDIF
               ENDIF
          ENDIF
     ENDPROC

Level: 14 Program File: basedataclasses.fxp Module/Object: stcustom.messagecentral Source File: C:\stic foxpro framework\base classes\basedataclasses.prg Line Number: 13102

A WHOLE BUNCH of stuff is missing from the stack trace at this point. We only get here when the error-handling code has prepared a parameter object (as seen above) and passed it to MessageCentral(). At this point, MessageCentral() proceeds without error to log the error, email the error log to me, and then, presumably, display it on the user's screen in a MESSAGEBOX(), although users often report they never see an error message.

Level: 15 Program File: messageutils.fxp Module/Object: messagecentral.messagecentral Source File: C:\stic foxpro framework\base classes\messageutils.prg Line Number: 272 Level: 16 Program File: messageutils.fxp Module/Object: messagecentral.ologger.logit Source File: C:\stic foxpro framework\base classes\messageutils.prg Line Number: 1031 Level: 17 Program File: messageutils.fxp Module/Object: messagecentral.ologger.loganerror Source File: C:\stic foxpro framework\base classes\messageutils.prg Line Number: 1286

Thank you, anyone who has had the forebearance to look at this.

Ken Dibble
www.stic-cil.org


_______________________________________________
Post Messages to: [email protected]
Subscription Maintenance: http://mail.leafe.com/mailman/listinfo/profox
OT-free version of this list: http://mail.leafe.com/mailman/listinfo/profoxtech
Searchable Archive: http://leafe.com/archives/search/profox
This message: 
http://leafe.com/archives/byMID/profox/[email protected]
** All postings, unless explicitly stated otherwise, are the opinions of the 
author, and do not constitute legal or medical advice. This statement is added 
to the messages for those lawyers who are too stupid to see the obvious.

Reply via email to