Jump to content

Programming Reference:Error Handling: Difference between revisions

From BCI2000 Wiki
Mellinger (talk | contribs)
No edit summary
 
Mellinger (talk | contribs)
 
(27 intermediate revisions by 4 users not shown)
Line 1: Line 1:
==Handling Errors==


\maketitle
\tableofcontents
\pagebreak
==Handling Errors==
(secHandl)
===Types of Errors===
===Types of Errors===
We assume that all errors we need to consider fall  
 
into one the following categories, each of which
We assume that all errors we need to consider fall into one of the following categories, each of which implies a different type of approach to error avoidance/error handling:
implies a different type of approach to error
*Parameter Setup Errors
avoidance/error handling:
*Runtime Errors
  \item{Parameter Setup Errors} \item{Runtime Errors} \item{Logic (Programming) Errors}
*Logic (Programming) Errors  
 
 
===Parameter Setup Errors===
===Parameter Setup Errors===
====Definition of the Term====
====Definition of the Term====
This category covers anything a user can do wrong
This category covers anything a user can do wrong by using a program with parameters that are out of range, inconsistent, or otherwise erroneous (e.g., by specifying an output file at a location where the user has no write permission).
by using a program with parameters that are out of range,
 
inconsistent, or otherwise erroneous (by, e.g.,
specifying an output file at a location where the user
has no write permission).
Parameter setup errors, when unhandled, become runtime errors.
Parameter setup errors, when unhandled, become runtime errors.
====Strategies====
====Strategies====
(secParamcheck)
 
As a guideline for approaching Parameter Setup Errors
As a guideline for approaching Parameter Setup Errors we adopt the following principle: "Whatever a
we adopt the following principle: "Whatever a
user does from within an application program should never make that application crash."
user does from within an application program should never
 
make that application crash."
In BCI2000, this translates into a thorough parameter check done by each module before any parameter settings are actually applied to the system.
In BCI 2000, this translates into a thorough parameter
 
check done by each module before any parameter settings are
actually applied to the system.
Parameter checking should comprise
Parameter checking should comprise
  \item{Range and Consistency checks,} whereby generally ranges depend on other parameters' values; \item{Signal property checks:} Does the output signal of one filter meet the next filter's requirements for its input signal? \item{Resource availability checks:}
*Range and Consistency checks, whereby ranges generally depend on the values of other parameters;  
  \item{Are needed system resources available?} (E.g., is it possible to open a required sound output device?) \item{Are auxiliary files (e.g., media files) available and readable?} \item{Do output files have legal file names? Are output files writeable?} (We could even check whether there is enough space left to write the EEG file, but practically this would not make too much sense because a concurrent process might use up the space while our system runs.)  
*Signal property checks: Does the output signal of one filter meet the next filter's requirements for its input signal?
*:
*Resource availability checks:
In each of those cases, the user should get appropriate
**Are needed system resources available? (E.g., is it possible to open a required sound output device?)
feedback guiding her towards fixing the problem.
**Are auxiliary files (e.g., media files) available and readable?
Whenever the system tries to fix a parameter setup error
**Do output files have legal file names? Are output files writeable? (We could even check whether there is enough space left to write the EEG file, but this is not practical because a concurrent process might use up the space while our system runs.)  
by using some default set of parameters, it should do so
 
only if
 
 
In each of these cases, the user should get appropriate feedback guiding her towards fixing the problem.
 
Whenever the system tries to fix a parameter setup error by using some default set of parameters, it should do so only if
 
*it presents the user with a warning that tells her what it did and why it did so, and if  
*it presents the user with a warning that tells her what it did and why it did so, and if  
*the automatically fixed parameters are treated as if changed by the user, i.e. with a parameter check performed on them.  
*the automatically fixed parameters are treated as if changed by the user, i.e. with a parameter check performed on them.  
Otherwise, people might end up using a system that doesn't
 
do what they want it to -- but without telling them, so they
Otherwise, people may unknowingly using a system that doesn't do what they want it to, or have
don't ever realize --, or with a system creating new parameter
a system that creates new parameter inconsistencies when trying to fix others.
inconsistencies when trying to fix others.
 
====User Interface Details====
====User Interface Details====
The user interface for Parameter Setup Error handling is,
 
along with the parameter setup dialog, part of the operator
The user interface for Parameter Setup Error handling is, along with the parameter setup dialog, part of the operator module. A first implementation of a GUI based user interface consists in a floating, non-modal error window popping up from the operator module that presents a list of error related textual messages to the user, allowing for browsing error messages
module. A first implementation of a GUI based user interface
while changing respective parameters via the parameter setup dialog. After the next parameter check, the operator module will close that window or replace its contents based on the result of the check.
consists in a floating, non-modal error window popping up from the operator
Parameter checking occurs when the user clicks the "SetConfig" button in the operator main window, followed by actually applying parameters if the check was successful.
module that presents a list of error related textual
 
messages to the user, allowing for browsing error messages
 
while changing respective parameters via the parameter setup
dialog. After the next parameter check, the operator module will close
that window or replace its contents based on the result of the check.
Parameter checking occurs when the user clicks the "SetConfig"
button in the operator main window, followed by actually applying
parameters in case the check was successful.
===Runtime Errors===
===Runtime Errors===
====Definition of the Term====
====Definition of the Term====
This category covers everything that can go wrong
 
in the course of an application program running
This category covers everything that can go wrong in the course of running an application program,
insofar as that malfunction is due to a lack of resources in the
insofar as that malfunction is due to a lack of resources in the underlying system required for proper operation (i.e., not due to a programming error). Assuming that parameter checking has been implemented properly as outlined above, we can narrow the term `Runtime Error' to cases for which the following statement holds: A runtime error occurs whenever the system runs out of resources that were still available during parameter checking.
underlying system required for proper operation
 
(i.e., not due to a programming error).
Assuming that parameter checking has been implemented
properly as outlined above, we can narrow the term
`Runtime Error' to cases for which the following statement
holds: A runtime error occurs whenever the system runs
out of resources that were still available during parameter
checking.
Typical reasons for this kind of error are
Typical reasons for this kind of error are
 
*the system runs out of disk space while recording data,  
*the system running out of disk space while recording data,  
*files being moved, trashed, or locked by a concurrent process,  
*files being moved, trashed, or locked by a concurrent process,  
*a network connection becoming unavailable.  
*a network connection becomes unavailable.  
Runtime errors, when unhandled, become logic errors because
 
the code implies assumptions that no longer hold once a
Runtime errors, when unhandled, become logic errors because the code implies assumptions that no longer hold once a runtime error has occurred.
runtime error has occurred.
 
====Strategies====
====Strategies====
In a properly designed and implemented system, runtime errors
 
in the restricted sense described above will not occur  
In a properly designed and implemented system, runtime errors, in the restricted sense described above, will not occur frequently. However, as they are caused by undesired circumstances outside the scope of the application program itself, it seems important to provide information to the user that is as detailed as possible in order to enable her to prevent this type of situation in the future, and to make her aware of the fact that the application program depends on her willingness to provide a smooth operating environment. After displaying a message of this kind, it seems appropriate to simply abort execution altogether, while trying to avoid a loss of the data acquired up to that time.
frequently. However, as they are caused by undesired
 
circumstances outside the scope of the application program
itself, it seems important to provide information
to the user as detailed as possible in order to enable her
to prevent this type of situation in the future, and to  
make her aware of the fact that the application program
depends on her willingness to provide a smooth operating
environment. This being ensured, it seems appropriate to
simply abort execution altogether, while trying to avoid  
a loss of the data acquired up to that time.
====User Interface Details====
====User Interface Details====
In general, it is desirable to have runtime errors displayed
 
along with the operator module's user interface. However,
In general, it is desirable to have runtime errors displayed along with the user interface of the operator module. However, as this requires a working connection between the module where the error occurs, and the operator module, this may not always be possible. Therefore, in addition to an operator-based error reporting interface, each module should have a less demanding mechanism to provide error information to the user, e.g., a local log file.
as this requires a working connection between the module where
 
the error occurs, and the operator module, this may not
 
always be possible. Therefore, in addition to an
operator-based error reporting interface, each module should
have a less demanding mechanism to provide error information to the
user, e.g., a local log file.
===Logic Errors===
===Logic Errors===
====Definition of the Term====
====Definition of the Term====
Logic, or programming, errors in general are faults of
 
a programmer who, in his or her code, implicitly or explicitly
Logic, or programming, errors in general can be due to a programmer who, in his or her code, implicitly or explicitly makes assumptions that do not always hold.
makes assumptions that do not always hold.
 
====Strategies====
====Strategies====
Programming errors are not supposed to occur at all in
 
a tested version of an application. Therefore, instead
Programming errors are not supposed to occur at all in a tested version of an application. Therefore, instead of trying to 'handle' them, it is important to make them show up as close to their point of origin in the code as possible, by frequently and explicitly checking whether
of trying to 'handle' them, it is important to make them show
implicit assumptions actually hold, and aborting execution with an error message if this is not the case.
up as close to their point of origin in the code as
 
possible, by frequently and explicitly checking whether
Typically, such checks use an "assertion" facility provided by the programming language. BCI2000 provides its own <tt>bciassert</tt> macro in <tt>BCIAssert.h</tt> to make sure that failed assertions result in messages that are displayed by the BCI2000 operator module. Unlike the standard assert macro, BCI2000 assertions do not evaluate to empty in release builds.
implicit assumptions actually hold, and aborting execution
 
with an error message if this is not the case.
Aside from that, writing code as explicit, general, and simple as possible greatly reduces the possibility of making logic errors in the first place.
Aside from that, writing code as explicit, general, and
 
simple as possible greatly reduces the possibility of making
logic errors in the first place.
====User Interface Details====
====User Interface Details====
As programming errors are nothing a user can do anything
 
about, and as their occurrence with a user is some sort of
As programming errors are errors about which a user can do nothing, simply aborting the program or module with an error message seems appropriate.
glitch anyway, simply aborting the program or module with
 
an error message seems appropriate.
 
\pagebreak
==Implementation Details==
==Implementation Details==
===Interface to the Programmer===
===Interface to the Programmer===
====Reporting Errors====
====Reporting Errors====
For a simple and general way to provide
user communication and error reporting means to a module's
programmer, there exist two global
objects derived from <tt>std::ostream</tt>, named
<tt>bciout</tt> and <tt>bcierr</tt>, in analogy to
<tt>std::cout</tt> and <tt>std::cerr</tt>, where <tt>bciout</tt>
is used to transfer general messages and warnings while
<tt>bcierr</tt> takes actual errors.


For a simple and general way to provide user communication and a means of error reporting to a module's programmer, there exist two global objects derived from <tt>std::ostream</tt>, one named
<tt>bciout</tt> and the other named <tt>bcierr</tt>, analogous to <tt>std::cout</tt> and <tt>std::cerr</tt>, where <tt>bciout</tt> is used to transfer general messages and warnings while <tt>bcierr</tt> takes actual errors.
A code example then looks like this:


  #include "BCIError.h"
  ...
  using namespace std;
  ...
  ofstream outputStream( fileName );
  if( !outputStream.is_open() )
  {
    bcierr << "Cannot open the file \""
          << fileName
          << "\" for output"
          << endl;
  }


Furthermore, for handling runtime errors from which it is difficult to recover, code may throw an exception that will abort execution and eventually lead to an error message being sent to the operator module (for framework related details see the section entitled "Implementation on the Framework Side"):


#include "BCIException.h"
...
if( ernie.find( bert ) != ernie.end() )
    throw bciexception( "Ernie just ate Bert. I don't know how to tell the story." );
tellMyStory( ernie, bert );
...


A code example then looks like this:
====Checking Parameters====
<pre>
using namespace std;
...
ofstream outputStream( fileName );
if( !outputStream.is_open() )


  bcierr << "Cannot open the file \""
Checking parameters is done in a separate member function of the filter base class which, similar to the member function that does the actual processing, takes input and output signal representatives as parameters, thus allowing for signal property checking.
        << fileName
        << "\" for output"
        << endl;


</pre>
Furthermore, for handling runtime errors difficult to recover
from, a programmer may also throw an exception that will abort
execution and
eventually lead to an error message being sent to the operator
module (for framework related details see section~(secImpl)):
<pre>
...
if( ernie.find( bert ) != ernie.end() )
  throw "Ernie just ate Bert. "
        "I don't know how to tell the story.";
tellMyStory( bert.begin(), ernie.end() );
...
</pre>
====Checking Parameters====
Checking parameters is done in a separate member function
of the filter base class which, similar to the member function
that does the actual processing, takes input and output signal
representatives as parameters, thus allowing for signal
property checking.
For the actual implementation, its declaration is as follows:
For the actual implementation, its declaration is as follows:
<pre>
 
void GenericFilter::Preflight(
void GenericFilter::Preflight ( const SignalProperties& Input,  
  const SignalProperties& inSignalProperties,
                                      SignalProperties& Output ) const;
        SignalProperties& outSignalProperties ) const;
 
</pre>
For a filter class derived from <tt>GenericFilter</tt>, this function is supposed to perform
For a filter class derived from
parameter checking as described in the "Strategies" section. Instead of returning an error value, it writes possible error messages into <tt>bcierr</tt>. Furthermore, it communicates dimensions of its output signal which it guarantees not to exceed, and it does so by adjusting the properties of the second SignalProperties object in its argument list, e.g.
<tt>GenericFilter</tt>, this function is supposed to perform
 
parameter checking as described in section~(secParamcheck).
Output = SignalProperties( Input.Channels(), 1 );
Instead of returning an error value, it writes possible error
 
messages into <tt>bcierr</tt>.
Furthermore, it communicates dimensions of its output signal
which it guarantees not to exceed, and it does so by adjusting
the properties of the second SignalProperties object in its
argument list, e.g.
<pre>
outSignalProperties
  = SignalProperties( inSignalProperties.Channels(), 1 );
</pre>
or
or
<pre>
 
outSignalProperties = SignalProperties( 0, 0 );
Output = SignalProperties( 0, 0 );
</pre>
 
if it declares not to use its output signal.
if it declares not to use its output signal.
The <tt>const</tt> declaration for its <tt>this</tt> pointer
 
prohibits initialization functionality from
The <tt>const</tt> declaration for its <tt>this</tt> pointer prohibits initialization functionality from <tt>GenericFilter::Initialize()</tt> entering into <tt>Preflight()</tt>. Such behavior is unwanted because it would corrupt the idea of performing a ''complete''  parameter check before actually ''altering'' the state of a filter object.
<tt>GenericFilter::Initialize()</tt> entering into
 
<tt>Preflight()</tt>; this is unwanted because it would
A necessary condition for a correct implementation of the <tt>Preflight()</tt> function is that any parameter, as well as any state that will be accessed during the processing phase, be accessed
corrupt the idea of performing a ''complete''  parameter check
from <tt>Preflight()</tt> at least once. For parameters and states defined by the filter itself (i.e. inside its constructor), range and accessibility checks are automatically performed by the framework; parameters and states defined by other filters must be explicitly accessed from <tt>Preflight()</tt>. If a <tt>GenericFilter</tt> descendant fails to access an externally defined parameter or state during <tt>Preflight()</tt>, the first access during the processing phase will result in a runtime error.
before actually ''altering'' the state of a filter object.
 
A necessary condition for a correct implementation of the
<tt>Preflight()</tt> function is that any parameter, as well as any
state that will be accessed during the processing phase, be accessed
from <tt>Preflight()</tt> at least once. For parameters and states
defined by the filter itself (i.e. inside its constructor), range
and accessibility checks are automatically performed by the framework;
parameters and states defined by other filters must be explicitly accessed
from <tt>Preflight()</tt>. If a <tt>GenericFilter</tt> descendant fails to
access an externally defined parameter or state during <tt>Preflight()</tt>,
the first access during the processing phase will result in a runtime error.
====Accessing Environment Objects====
====Accessing Environment Objects====
Parameters and states are considered to constitute an "environment", and a
 
<tt>GenericFilter</tt> descendant to live in that environment, in analogy to
We consider parameters and states part of the BCI2000 "environment". <tt>GenericFilter</tt> descendants have access to that environment, analogous to the concept of environment variables found in some operating systems. Internally, access to the environment is mediated through a mix-in-class named <tt>Environment</tt> that provides accessor symbols to a filter programmer.
the concept of environment variables found in some operating systems.
 
Internally, access to the environment is mediated through a mix-in-class named
'''Low Level Access to Environment Objects''' is provided by the following symbols:
<tt>Environment</tt> that provides accessor symbols to a filter programmer.
*<tt>Parameters</tt> syntactically behaves like a <tt>ParamList*</tt>,
\paragraph{Low Level Access to Environment Objects}
*<tt>States</tt> behaves like a <tt>StateList*</tt>,
is provided by the following symbols:
*and <tt>Statevector</tt> behaves like a <tt>StateVector*</tt>.
 
As an example,
 
float  myParameterValue = 0.0;
Param* param = Parameters->GetParamPtr( "MyParameter" );
if( param )
  myParameterValue = atof( param->GetValue() );
else
  bcierr << "Could not access \"MyParameter\"" << endl;
 
Unlike true pointers, these symbols cannot be assigned any values, cannot be assigned to variables, or have other manipulating operators applied. For example, the lines
 
delete Parameters;
Parameters = new ParamList;
 
will all result in compiler errors.
 
'''Convenient Access to Environment Objects''' is possible through a number of symbols which offer built-in checking and error reporting:
    
    
*<tt>Parameters</tt> syntactically behaves like a <tt>PARAMLIST*</tt>,
*<tt>Parameter(Name)[(Index 1[, Index 2])</tt>  
*<tt>States</tt> behaves like a <tt>STATELIST*</tt>,
 
*and <tt>Statevector</tt> behaves like a <tt>STATEVECTOR*</tt>.
This symbol stands for the value of the named parameter.  Indices may be given in numerical or textual form; if omitted, they default to 0. The type of the symbol <tt>Parameter()</tt> may be numerical or a string type, depending on its use. (If the compiler complains about ambiguities, use explicit typecasts.) If a parameter with the given name does not exist, an error message is written into <tt>bcierr</tt>. If the specified indices do not exist, no error is reported. In both cases, on read access, the string constant <tt>"0"</tt> resp. the number 0 is returned.
As an example, take
 
<pre>
Examples:  
float  myParameterValue = 0.0;
 
PARAM* param = Parameters->GetParamPtr( "MyParameter" );
int myValue = Parameter( "MyParam" );  
if( param )
string myOtherValue = Parameter( "MyOtherParam" );  
  myParameterValue = atof( param->GetValue() );
Parameter( "My3rdParam" )( 2, 3 ) = my3rdValue;  
else
 
  bcierr << "Could not access \"MyParameter\"" << endl;
*<tt>OptionalParameter(Name[, Default Value])(Index 1[, Index 2])</tt>  
</pre>
 
Unlike true pointers, these symbols cannot be assigned any values,
This symbol behaves like the symbol <tt>Parameter()</tt> but will not report an error if the parameter does not exist. Instead, it will return the default value given in its first argument. Assignments to this symbol are not possible.  
cannot be assigned to variables, nor other manipulating operators applied.
 
E.g., the lines
*<tt>State(Name)</tt>  
<pre>
 
delete Parameters;
This symbol allows for reading a state's value from the state vector and setting a state's value in the state vector. Trying to access a state that is not accessible will result in an error reported via <tt>bcierr</tt>.  
Parameters = new PARAMLIST;
 
PARAMLIST* someParamlistPointer = Parameters;
Examples:
</pre>
 
will all result in compiler errors.\footnote{In the current (preliminary)
short currentStateOfAffairs = State( "OfAffairs" );  
implementation, assignments from these symbols as in the last example are
State( "OfAffairs" ) = nextStateOfAffairs;
allowed to ease the transition process.}
 
\paragraph{Convenient Access to Environment Objects}
*<tt>OptionalState(Name[, Default Value])</tt>  
is possible through a number of symbols which offer
 
built-in checking and error reporting:
Analagous to <tt>OptionalParameter()</tt>, this symbol does not report an error if the specified state does not exist but returns the given default value. Assignments to this symbol are not possible.  
 
 
*{<tt>Parameter(Name[, Index 1[, Index 2]])</tt>} This symbol stands for the value of the named parameter.  Indices may be given in numerical or textual form; if omitted, they default to 0. The type of the symbol <tt>Parameter()</tt> may be numerical or a string type, depending on its use.\footnote{If the compiler complains about ambiguities, use explicit typecasts as in the second example.} If a parameter with the given name does not exist, an error message is written into <tt>bcierr</tt>. If the specified indices do not exist, no error is reported. In both cases, on read access, the string constant <tt>"0"</tt> resp. the number 0 is returned. Examples: <pre> int myValue = Parameter( "MyParam" ); string myOtherValue = ( const char* )Parameter( "MyOtherParam" ); Parameter( "My3rdParam", 2, 3 ) = my3rdValue; </pre>
*<tt>PreflightCondition(Condition)</tt>
*<tt>OptionalParameter(Default Value, Name[, Index 1[, Index 2]])</tt> This symbol behaves like the symbol <tt>Parameter()</tt> but will not report an error if the parameter does not exist. Instead, it will return the default value given in its first argument. Assignments to this symbol are not possible.  
 
*<tt>State(Name)</tt> This symbol allows for reading a state's value from the state vector by assigning from it, and setting a state's value in the state vector by assigning to it. Trying to access a state that is not accessible will result in an error reported via <tt>bcierr</tt>. Examples: <pre> short currentStateOfAffairs = State( "OfAffairs" ); State( "OfAffairs" ) = nextStateOfAffairs; </pre>
This symbol is meant to be used inside implementations of  <tt>GenericFilter::Preflight()</tt>. If the boolean condition given as its argument is false, it will output an error message into <tt>bcierr</tt> containing the condition given in its argument.  
*<tt>OptionalState(Default Value, Name)</tt> In analogy to <tt>OptionalParameter()</tt> this symbol does not report an error if the specified state does not exist but returns the given default value. Assignments to this symbol are not possible.  
 
*<tt>PreflightCondition(Condition)</tt> This symbol is meant to be used inside implementations of  <tt>GenericFilter::Preflight()</tt>. If the boolean condition given as its argument is false, it will output an error message into <tt>bcierr</tt> containing the condition given in its argument. Example: <pre> PreflightCondition(   Parameter( "TransmitCh" ) <= Parameter( "SourceCh" ) ); </pre> If TransmitCh is greater than SourceCh, a message will be sent to <tt>bcierr</tt> and displayed to the user, stating:\footnote{In future versions, the error may be reported in natural language form generated from the boolean expression.} <pre> Condition not fulfilled:  Parameter( "TransmitCh" ) <= Parameter( "SourceCh" ) </pre>  
Example:  
 
<tt>PreflightCondition( Parameter( "TransmitCh" ) <= Parameter( "SourceCh" ) );</tt>  
 
If TransmitCh is greater than SourceCh, a message will be sent to <tt>bcierr</tt> and displayed to the user, stating:  
 
<tt>A necessary condition is violated. Please make sure that the following is true:   
Parameter( "TransmitCh" ) <= Parameter( "SourceCh" )</tt>
 
===Implementation on the Framework Side===
===Implementation on the Framework Side===
(secImpl)
 
The operator module's behaviour in response to an error
The behaviour of the operator module in response to an error message that arrives from one of the modules depends on its context, i.e., on the execution phase the system is in. That way, no additional programming interface elements visible to a filter/module programmer are needed to implement an error handling scheme as described in the "Handling Errors"section.  
message arriving from one of the modules depends
 
on its context, i.e. on the execution phase the system is in.
During the '''preflight phase,'''  errors are ''Configuration Errors''. A module's framework code behind <tt>bcierr</tt> just collects error messages; on return from the preflight function, it
That way, no additional programming interface elements
sends those messages to the operator module which then, from the contents of the message (i.e., whether it was empty or not), determines whether the preflight was successful; on not receiving any message after some timeout it assumes a broken connection or a crashed module.
visible to a filter/module programmer
 
are needed to implement an error handling scheme as
(For now, a simple timeout scheme with a fixed timeout interval of 5s seems appropriate. In the future, one might consider a module requesting additional timeout periods if it expects lengthy calculations.)
described in section~(secHandl).  
 
During the '''preflight phase,'''  errors are {Parameter
During all '''other phases,'''  the code behind <tt>bcierr</tt> immediately (i.e., on flushing the <tt>std::ostream</tt>) sends its message buffer to a log file as well as to the operator module, indicating a ''Runtime Error'' to the operator module which will, in turn, halt the system,shut down the other modules, and display the message to the user.
Setup Errors.} A module's framework code behind
 
<tt>bcierr</tt> just collects error
In addition, the top level exception handling code of each module contains similar functionality,
messages; on return from the preflight function, it
sending an exception's associated description string into a log file and to the operator module, if possible, then quitting the module in which the exception occurred. This not only ensures
sends those messages to the operator module which then,
a proper general handling of exceptions within the framework but also allows a programmer to handle ''Runtime Errors'' by raising her own exceptions, eliminating the need to take care of the error
from the contents of the message (i.e. whether it was empty
or not), determines whether the preflight was successful;
on not receiving any message after some timeout\footnote{
For now, a simple timeout scheme with a fixed timeout
interval of 5~s seems appropriate. In the future, one might consider
a module requesting additional timeout periods if it expects  
lengthy calculations.}
it assumes a broken connection or a crashed module.
During all '''other phases,'''  the code behind <tt>bcierr</tt>
immediately (i.e., on flushing the <tt>std::ostream</tt>)
sends its message buffer to a log file as well as to
the operator module, indicating a {Runtime Error}
to the operator module which will, in turn, halt the system,
shut down the other modules, and display the message to the
user.
In addition, the top level exception handling code
of each module contains similar functionality,
sending an exception's associated description string into
a log file and to the operator module, if possible, then
quitting the module in which the exception occurred.
This not only ensures
a proper general handling of exceptions within the framework but
also allows a programmer to handle {Runtime Errors} by raising
her own exceptions, eliminating the need to take care of the error
condition in the code following the detection of an error.
condition in the code following the detection of an error.
----
==See also==
[[Programming Reference:Errors and Warnings]], [[Programming Reference:Environment Class]], [[Programming Reference:Debug Output]]
[[Category:Framework API]][[Category:Development]]

Latest revision as of 14:19, 12 August 2011

Handling Errors

Types of Errors

We assume that all errors we need to consider fall into one of the following categories, each of which implies a different type of approach to error avoidance/error handling:

  • Parameter Setup Errors
  • Runtime Errors
  • Logic (Programming) Errors


Parameter Setup Errors

Definition of the Term

This category covers anything a user can do wrong by using a program with parameters that are out of range, inconsistent, or otherwise erroneous (e.g., by specifying an output file at a location where the user has no write permission).

Parameter setup errors, when unhandled, become runtime errors.

Strategies

As a guideline for approaching Parameter Setup Errors we adopt the following principle: "Whatever a user does from within an application program should never make that application crash."

In BCI2000, this translates into a thorough parameter check done by each module before any parameter settings are actually applied to the system.

Parameter checking should comprise

  • Range and Consistency checks, whereby ranges generally depend on the values of other parameters;
  • Signal property checks: Does the output signal of one filter meet the next filter's requirements for its input signal?
  • Resource availability checks:
    • Are needed system resources available? (E.g., is it possible to open a required sound output device?)
    • Are auxiliary files (e.g., media files) available and readable?
    • Do output files have legal file names? Are output files writeable? (We could even check whether there is enough space left to write the EEG file, but this is not practical because a concurrent process might use up the space while our system runs.)


In each of these cases, the user should get appropriate feedback guiding her towards fixing the problem.

Whenever the system tries to fix a parameter setup error by using some default set of parameters, it should do so only if

  • it presents the user with a warning that tells her what it did and why it did so, and if
  • the automatically fixed parameters are treated as if changed by the user, i.e. with a parameter check performed on them.

Otherwise, people may unknowingly using a system that doesn't do what they want it to, or have a system that creates new parameter inconsistencies when trying to fix others.

User Interface Details

The user interface for Parameter Setup Error handling is, along with the parameter setup dialog, part of the operator module. A first implementation of a GUI based user interface consists in a floating, non-modal error window popping up from the operator module that presents a list of error related textual messages to the user, allowing for browsing error messages while changing respective parameters via the parameter setup dialog. After the next parameter check, the operator module will close that window or replace its contents based on the result of the check. Parameter checking occurs when the user clicks the "SetConfig" button in the operator main window, followed by actually applying parameters if the check was successful.


Runtime Errors

Definition of the Term

This category covers everything that can go wrong in the course of running an application program, insofar as that malfunction is due to a lack of resources in the underlying system required for proper operation (i.e., not due to a programming error). Assuming that parameter checking has been implemented properly as outlined above, we can narrow the term `Runtime Error' to cases for which the following statement holds: A runtime error occurs whenever the system runs out of resources that were still available during parameter checking.

Typical reasons for this kind of error are

  • the system runs out of disk space while recording data,
  • files being moved, trashed, or locked by a concurrent process,
  • a network connection becomes unavailable.

Runtime errors, when unhandled, become logic errors because the code implies assumptions that no longer hold once a runtime error has occurred.

Strategies

In a properly designed and implemented system, runtime errors, in the restricted sense described above, will not occur frequently. However, as they are caused by undesired circumstances outside the scope of the application program itself, it seems important to provide information to the user that is as detailed as possible in order to enable her to prevent this type of situation in the future, and to make her aware of the fact that the application program depends on her willingness to provide a smooth operating environment. After displaying a message of this kind, it seems appropriate to simply abort execution altogether, while trying to avoid a loss of the data acquired up to that time.

User Interface Details

In general, it is desirable to have runtime errors displayed along with the user interface of the operator module. However, as this requires a working connection between the module where the error occurs, and the operator module, this may not always be possible. Therefore, in addition to an operator-based error reporting interface, each module should have a less demanding mechanism to provide error information to the user, e.g., a local log file.


Logic Errors

Definition of the Term

Logic, or programming, errors in general can be due to a programmer who, in his or her code, implicitly or explicitly makes assumptions that do not always hold.

Strategies

Programming errors are not supposed to occur at all in a tested version of an application. Therefore, instead of trying to 'handle' them, it is important to make them show up as close to their point of origin in the code as possible, by frequently and explicitly checking whether implicit assumptions actually hold, and aborting execution with an error message if this is not the case.

Typically, such checks use an "assertion" facility provided by the programming language. BCI2000 provides its own bciassert macro in BCIAssert.h to make sure that failed assertions result in messages that are displayed by the BCI2000 operator module. Unlike the standard assert macro, BCI2000 assertions do not evaluate to empty in release builds.

Aside from that, writing code as explicit, general, and simple as possible greatly reduces the possibility of making logic errors in the first place.

User Interface Details

As programming errors are errors about which a user can do nothing, simply aborting the program or module with an error message seems appropriate.


Implementation Details

Interface to the Programmer

Reporting Errors

For a simple and general way to provide user communication and a means of error reporting to a module's programmer, there exist two global objects derived from std::ostream, one named bciout and the other named bcierr, analogous to std::cout and std::cerr, where bciout is used to transfer general messages and warnings while bcierr takes actual errors. A code example then looks like this:

 #include "BCIError.h"
 ...
 using namespace std;
 ...
 ofstream outputStream( fileName );
 if( !outputStream.is_open() )
 {
   bcierr << "Cannot open the file \""
          << fileName
          << "\" for output"
          << endl;
 }

Furthermore, for handling runtime errors from which it is difficult to recover, code may throw an exception that will abort execution and eventually lead to an error message being sent to the operator module (for framework related details see the section entitled "Implementation on the Framework Side"):

#include "BCIException.h"
...
if( ernie.find( bert ) != ernie.end() )
   throw bciexception( "Ernie just ate Bert. I don't know how to tell the story." );
tellMyStory( ernie, bert );
...

Checking Parameters

Checking parameters is done in a separate member function of the filter base class which, similar to the member function that does the actual processing, takes input and output signal representatives as parameters, thus allowing for signal property checking.

For the actual implementation, its declaration is as follows:

void GenericFilter::Preflight ( const SignalProperties& Input, 
                                      SignalProperties& Output ) const;

For a filter class derived from GenericFilter, this function is supposed to perform parameter checking as described in the "Strategies" section. Instead of returning an error value, it writes possible error messages into bcierr. Furthermore, it communicates dimensions of its output signal which it guarantees not to exceed, and it does so by adjusting the properties of the second SignalProperties object in its argument list, e.g.

Output = SignalProperties( Input.Channels(), 1 );

or

Output = SignalProperties( 0, 0 );

if it declares not to use its output signal.

The const declaration for its this pointer prohibits initialization functionality from GenericFilter::Initialize() entering into Preflight(). Such behavior is unwanted because it would corrupt the idea of performing a complete parameter check before actually altering the state of a filter object.

A necessary condition for a correct implementation of the Preflight() function is that any parameter, as well as any state that will be accessed during the processing phase, be accessed from Preflight() at least once. For parameters and states defined by the filter itself (i.e. inside its constructor), range and accessibility checks are automatically performed by the framework; parameters and states defined by other filters must be explicitly accessed from Preflight(). If a GenericFilter descendant fails to access an externally defined parameter or state during Preflight(), the first access during the processing phase will result in a runtime error.

Accessing Environment Objects

We consider parameters and states part of the BCI2000 "environment". GenericFilter descendants have access to that environment, analogous to the concept of environment variables found in some operating systems. Internally, access to the environment is mediated through a mix-in-class named Environment that provides accessor symbols to a filter programmer.

Low Level Access to Environment Objects is provided by the following symbols:

  • Parameters syntactically behaves like a ParamList*,
  • States behaves like a StateList*,
  • and Statevector behaves like a StateVector*.

As an example,

float  myParameterValue = 0.0;
Param* param = Parameters->GetParamPtr( "MyParameter" );
if( param )
  myParameterValue = atof( param->GetValue() );
else
  bcierr << "Could not access \"MyParameter\"" << endl;

Unlike true pointers, these symbols cannot be assigned any values, cannot be assigned to variables, or have other manipulating operators applied. For example, the lines

delete Parameters;
Parameters = new ParamList;

will all result in compiler errors.

Convenient Access to Environment Objects is possible through a number of symbols which offer built-in checking and error reporting:

  • Parameter(Name)[(Index 1[, Index 2])

This symbol stands for the value of the named parameter. Indices may be given in numerical or textual form; if omitted, they default to 0. The type of the symbol Parameter() may be numerical or a string type, depending on its use. (If the compiler complains about ambiguities, use explicit typecasts.) If a parameter with the given name does not exist, an error message is written into bcierr. If the specified indices do not exist, no error is reported. In both cases, on read access, the string constant "0" resp. the number 0 is returned.

Examples:

int myValue = Parameter( "MyParam" ); 
string myOtherValue = Parameter( "MyOtherParam" ); 
Parameter( "My3rdParam" )( 2, 3 ) = my3rdValue; 
  • OptionalParameter(Name[, Default Value])(Index 1[, Index 2])

This symbol behaves like the symbol Parameter() but will not report an error if the parameter does not exist. Instead, it will return the default value given in its first argument. Assignments to this symbol are not possible.

  • State(Name)

This symbol allows for reading a state's value from the state vector and setting a state's value in the state vector. Trying to access a state that is not accessible will result in an error reported via bcierr.

Examples:

short currentStateOfAffairs = State( "OfAffairs" ); 
State( "OfAffairs" ) = nextStateOfAffairs;
  • OptionalState(Name[, Default Value])

Analagous to OptionalParameter(), this symbol does not report an error if the specified state does not exist but returns the given default value. Assignments to this symbol are not possible.

  • PreflightCondition(Condition)

This symbol is meant to be used inside implementations of GenericFilter::Preflight(). If the boolean condition given as its argument is false, it will output an error message into bcierr containing the condition given in its argument.

Example:

PreflightCondition( Parameter( "TransmitCh" ) <= Parameter( "SourceCh" ) );

If TransmitCh is greater than SourceCh, a message will be sent to bcierr and displayed to the user, stating:

A necessary condition is violated. Please make sure that the following is true: Parameter( "TransmitCh" ) <= Parameter( "SourceCh" )

Implementation on the Framework Side

The behaviour of the operator module in response to an error message that arrives from one of the modules depends on its context, i.e., on the execution phase the system is in. That way, no additional programming interface elements visible to a filter/module programmer are needed to implement an error handling scheme as described in the "Handling Errors"section.

During the preflight phase, errors are Configuration Errors. A module's framework code behind bcierr just collects error messages; on return from the preflight function, it sends those messages to the operator module which then, from the contents of the message (i.e., whether it was empty or not), determines whether the preflight was successful; on not receiving any message after some timeout it assumes a broken connection or a crashed module.

(For now, a simple timeout scheme with a fixed timeout interval of 5s seems appropriate. In the future, one might consider a module requesting additional timeout periods if it expects lengthy calculations.)

During all other phases, the code behind bcierr immediately (i.e., on flushing the std::ostream) sends its message buffer to a log file as well as to the operator module, indicating a Runtime Error to the operator module which will, in turn, halt the system,shut down the other modules, and display the message to the user.

In addition, the top level exception handling code of each module contains similar functionality, sending an exception's associated description string into a log file and to the operator module, if possible, then quitting the module in which the exception occurred. This not only ensures a proper general handling of exceptions within the framework but also allows a programmer to handle Runtime Errors by raising her own exceptions, eliminating the need to take care of the error condition in the code following the detection of an error.


See also

Programming Reference:Errors and Warnings, Programming Reference:Environment Class, Programming Reference:Debug Output