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.