kers@hplb.hpl.hp.com (Chris Dollin) (11/24/90)
There's been a lot of discussion recently about coping with erros. A friend of mine has composed this, on which he'd like comments; post them or mail to me, please. Regards, Kers. ;;; -------------------------------------------------------------------------- PROGRAMMING FOR ERRORS Introduction Any programmer comes to terms with errors early in their career. Most people think of errors as things to be eradicated from programs, compilation errors, bugs and so on. This paper is not about that at all. Instead, I want to discuss the pitfalls involved in writing programs, and libraries of modules which behave 'well' even when faced with errors outside programmer control. For example, anyone can write a program which reads in two numbers and prints their sum. In C, for example, such a program is only a few lines long. However, try to write a program which does the same thing, but if faced with only one number, or invalid characters in the data or something, manages to respond with a helpful message, and even this simple program gets messy. Things become even worse when a program has to handle not only user mistakes, but also all the rare but possible things which can go wrong with the computer. How many programs have you seen which recover gracefully if the disk you are writing to is full, or too many files are open at once within the system, or ( in a multi-user system ) someone else has interfered with your data. My experience has been that writing in a language like C, a program which takes care of all such errors in a reasonable way is harder to write, harder to debug and harder to maintain than one which just ignores the possibility. Unfortunately, the program which ignores the possibility of these errors may end up corrupting data or losing work or just frustrating the user if one of those nasty things does happen. The rest of this paper will try to identify the various different things which are called errors, and to devise a reasonable way of dealing with them. This ultimately should, I believe, involve a number of changes to the definition of common programming languages to cope, but a lot can be done already. Later, my own implementation of a reasonable compromise in C++ will be described. The examples, and the experience on which they are based, are founded in the Unix/C world, but many aspects are equally relevant to other languages. A simple example It is always easiest to understand a problem by seeing an example. Here is what should be a simple program, and what happens to it when we start to worry about errors. #include <stdio.h> main() { int i; while(1) { scanf("%d",&i); if (i == -1) break; printf("The square is %d\n",i*i); } } This program, of course, prompts for a number. If it gets -1 it exits, otherwise it prints the square and goes round again. Given the following input 1 4 100 3 6 -1 it produces The square is 1 The square is 16 The square is 10000 The square is 9 The square is 36 as expected. Unfortunately, the program is not at all robust. The first problem arises if we give a non-number. You might expect the program to treat it as 0, or possibly to exit with an error report. Instead, it just loops forever. Thus: 3 hello as input gives The square is 9 The square is 9 The square is 9 The square is 9 The square is 9 The square is 9 The square is 9 ... and so on. The second problem occurs if the number we specify is too large for its square to be a valid integer. If you try 1000000 as input you get -727379968 or similar nonsense on a typical 32 bit machine. The third problem occurs if some operating system error happens. We might try to send output to a file not a terminal using the Unix > redirection, and the file could be on a disk slice which is full up. In that case, the output will be lost, but the program will continue anyway. In order to handle these errors, we need firstly to decide what to do in such cases. For example, it is probably quite reasonable simply to exit in the third case, perhaps with a warning message to stderr. ( If stderr is also damaged, we give up totally ). In the first case, we should print out a different message, say - invalid character found. while in the second case we need to say - is not within range. The modified program could look like this: #include <stdio.h> main() { int i,i2; int result; int ch; while(1) { result = scanf("%d",&i); if (result <= 0) { ch = getchar(); if (ch == EOF) break; /* We got an end of file */ result = printf("%c -invalid character found\n",ch); if (result < 0) goto abort; continue; } i2 = i * i; if (( i2 / i ) != i) { result = printf("%d - is not within range\n",i); if (result < 0) goto abort; } if (i == -1) break; result = printf("The square is %d\n",i*i); if (result < 0) goto abort; } return(0); abort: fprintf(stderr,"Fatal error writing to stdout - aborting\n"); return(1); } The points to note are: - notice the use of a goto to handle a fatal error. This is a theme we will expand on below. - the modified program is much larger than the original, less easy to follow, and contains at least one nasty trick ( the division to see if the square was out of range ) The cleaning up problem and exceptions The example we have looked at was particularly simple, because there was no concern about what to do to exit a routine cleanly. Quite a lot of routines, especially those neither at top level or bottom level, may need to do some kind of cleaning up before they exit. This issue affects error handling because a very common reason to want to leave a routine without going all the way through is as a result of an error. However, it applies equally to routines which may do some setting up, processing, and clearing up afterwards. As an example, suppose we have a program which reads records from a file, and returns the first record which meets certain conditions. We want to guarantee that the file is closed before the routine exits. Our first attempt might look like this: doit() { int fd = open_the_file; if file not opened return error; while(1) { get a record from fd; if (end of file) { close the file; return failure; } if (some other error) { close the file return the error } check the record; if found { close the file; return success; } } } For the moment, we won't worry about how we distinguish success, failure or an error ( we might return 0 for success, -1 for failure, and 1 for an error, in which case we set up a global error variable with the error type ). The problem with the above solution is that the clearing up code occurs three times, and although it is fairly simple, it is still pretty bad practice to repeat the same stuff in different places. A better solution would be: clearup() { close the file; } doit() { int fd = open_the_file; if file not opened return error; while(1) { get a record from fd; if (end of file) { clearup; return failure; } if (some other error) { clearup; return the error } check the record; if found { clearup; return success; } } } Unfortunately, that requires clearup to have access to the local fd which is not in its scope. Some languages, eg pascal, would allow this, by making clearup a routine defined within the scope of doit, but C does not. We could make fd global, or pass it as a parameter, but such ideas get out of hand if the clearup process is more complicated - precisely the circumstance we are most concerned about. So, consider the following further possibility: doit() { return_value result; int fd = open_the_file; if file not opened return error; while(1) { get a record from fd; if (end of file) { } if (some other error) { result = the error; goto clearup; } check the record; if found { result = success goto clearup; } } clearup: close the file return result } This solution looks bad to those who have learned to treat goto's with suspicion, but has in fact a lot going for it. Firstly, the clearup code is guaranteed to be obeyed, regardless of how the routine is exited. We simply replace the usual return(result) code with result = value;goto clearup; something we could even hide in a C macro. Secondly, the clearup code is right there in the function, not hidden away elsewhere. Thirdly, the code automatically has access to all of the variables of the function, so it could, for example, conditionally close files depending on whether they had in fact been opened, or whatever. This particular style of programming has led to a number of languages, including ADA and GNU C++ providing support for a so-called exception construct. This is essentially the same as a goto as above, with the following differences: a) An exception must be structured with proper scope, so that you can have nested exceptions in a way which enforces the control flow. b) One can pass exceptions as parameters to routines, to allow them to call the exception directly rather than having to put the goto in at the higher level each time. However, exceptions don't solve all our problems. We still need that annoying extra result variable to allow us to decide first what to return, and then raise the exception. Furthermore, if a routine uses exception results, every routine which calls it needs to know. It is also very difficult to arrange for exceptions to essentially do nothing, and return control to the main program anyway. Finally, exceptions don't actually address the issue of what precisely went wrong at a lower level. As a result, while exceptions are a useful alternative to gotos for clearup type work, they don't deal well with many kinds of error. To address that problem, we need to look at errors more closely. Errors, a closer look. It is time now to try to categorise the different types of error which can occur. 1) Precondition violation. As I explained in the introduction, this paper is not about programming errors or bugs as such. However, if I am writing a module of routines for use by separate programs, I can't guarantee that the calls to my modules will always pass the correct information. Some languages like C++ or Pascal will spot some basic errors ( wrong number or types of parameters ), while pre-ansi C will fail even to do that much at compile time. In general, it is impossible to guarantee that a module will be robust and behave sensibly regardless of such things as passing variables instead of pointers, missing out parameters, getting them in the wrong order, and all the other little difficulties beloved of C programmers. That problem is expressed by saying that the 'preconditions' for the module/routine are not met, which, in other words means that the behaviour of the module is undefined. If you are lucky, you may get a diagnostic dump with some clues as to what you did wrong. If not, the program may do something totally unexpected later on as a result of a 'scribble'. 2) 'Expected' incorrect input. A very common situation occurs when a routine is written to handle data which will have come directly and not yet checked from a user. Such routines must typically consider a large number of possible ways in which the input may fail to meet expectations, and what to do. This problem is epitomised by that of writing a compiler, where the quality of the compiler depends at least as much on how good the error messages are as on how good the code generated for an error free program may be. At a simpler level, our first example demonstrates this case also. The original program treated any invalid input as a precondition violation, in that it paid no attention to what would happen in that case. The second one widened its scope to treat some possible mistakes as 'expected' and deal with them better. In general, a well-written routine will treat as much as possible as 'expected' and always attempt to do something reasonable. Only where the language makes it impossible to do better should unexpected behaviour occur. That still begs the question of what 'reasonable' means. As we shall see, most languages make it rather hard to be reasonable all the time. 3) Failures during processing. The next case occurs when a routine has been given acceptable data, but at some point during its processing, cannot perform some task. This may be because the input data turns out not really to be acceptable after all, or because some system resource was unavailable or refused, or even because an internal code check showed up a bug in the routine's handling of the current data. In any event, the routine needs to identify the cause, and have some way of reporting what happened. There is not much one can do about precondition violations except to try wherever possible to move them into one of the other categories by widening the scope of the routine. On the other hand, expected errors will have an expected response, and the caller of the routine will know to check that response and behave accordingly. The rest of the paper will concentrate on the third case. From now on error will mean only error in that sense. The normal possibilities for error handling. Error handling presents a challenge at three different levels. First, there is the bottom level, i.e. the routine which first detects the error. This is commonly an operating system routine such as read or write, but it may be a user-written routine also. Secondly, there is the top level. This is either the main program, or possibly a routine called from the main program but specific to it. Finally, and as we shall see, hardest of all, there are intermediate routines. These routines may themselves detect errors and have to report them, but they may also call lower level routines which lead to errors, and have to be dealt with too. Let's look at the top level program first. At this level, we have a number of key bits of information which are not available further down. Firstly, we will know whether a detected error is serious and should result in immediate program termination, or trivial and safe to ignore, or requires evasive action of a non-fatal sort. Secondly, we will know how to report any errors back to the user. In some cases this may be via a screen-based window, in others to a system log. The point is that only the top level program can really decide because only it is guaranteed to know how and on what it is running. At the other extreme, the bottom level routine knows exactly what went wrong, but not necessarily what to do about it. Most languages give these alternatives: - ignore the error - send a report to an error device - crash the program with an error report - call a user-defined routine - return with a special value set to indicate the error or some combination of the above. Of these, we can immediately reject all but the last two. The other three pre-empt the decision about how to handle the error, rather than allowing that decision to be made higher up. So, let's consider the final two. - call a user-definedz routine allows the user at any time to specify a specific routine which will be called whenever an error ( or possibly only certain kinds of error) occurs. This routine may do any of the first three possible actions, or it may do something completely different to recover from the problem. At first sight, this approach, which corresponds to the Unix/C signal system used for so-called fatal errors, might appear to be the perfect solution. However, when we come to consider intermediate routines, it becomes clear that there are a number of problems. In the meantime, we can usefully list the advantages in those cases where this approach does work. Firstly, the hander routine can be set up by main, and so can be written in the full knowledge of the environment in which the program is operating. Secondly, once main has set up a handler, it can often then simply ignore the possibility of an error in its main code. This results in much cleaner and easier to read code. It also reduces the possibility that some routine may fail without the caller noticing. Thirdly, it is usually possible to arrange for a handler routine to perform either directly or indirectly via a goto any clean up of their system which may be needed before exiting. Thus, we can gain the benefits of the exception handling style described above. Let's compare that with the final possibility - simply flagging the error, either via a return or write-back parameter, or by using a global variable. This is the method favoured by C for most errors in the operating system interface. Usually a global integer errno is set to indicate the nature of an error, whose presence is indicated by passing an 'impossible' result back to the calling routine. This approach has the following advantages. Firstly, whenever things do go wrong, the calling routine has every opportunity to identify that fact, and even identify the precise nature of the problem so as to decide what to do next. Secondly, it is easy for an intermediate routine to decide that a lower level error is not serious, and cancel it altogether, or to replace it with a different more relevant error Thirdly, there is an extremely low overhead when no error does in fact occur. Fourthly, it is very easy to deal with errors in different places differently. Fifthly, if an exception/clearup is called for, this is easy to cause. There are, however, two disadvantages which matter. Firstly, it is up to the programmer to test explicitly for an error at every point. To forget to do so, or assume that one will not occur, may easily result in a major problem elsewhere in the program, a bug which is non-repeatable and hard to track. Secondly, code which does explicitly check for all possible errors ends up looking awful. A small sample gives the idea: Without error checking ..... fd = open(filename,O_RDONLY); write(fd,message,mlen); lseek(fd,position,0); read(fd,message2,mlen); .... With error checking fd = open(filename,O_RDONLY); if (fd < 0) { perror("open file"); exit(1); } i = write(fd,message,mlen); if (nread < 0) { perrror("write message"); exit(1); } i = lseek(fd,position,0); if (i < 0) { perror("lseek"); exit(1); } i = read(fd,message,mlen); if (i < 0) { perror("read"); exit(1); } if (i < mlen) { printf("Only %d bytes available\n",i); exit(1); } ....... Which is easier to follow? Of course, one could make things a bit easier by writing a general purpose message and exit routine, but that would only save a few lines, and in that case anyway, one would prefer to have used the user-defined-routine method. Things become even more complex when we consider the proper writing of intermediate level routines. In the user-defined routine case, we might want to handle certain errors ourselves, but a higher level may have set up a user-defined routine. So, assuming we can, we need to stack the higher level routine, set up our own, and then, depending on the error, promulgate it up by calling the stacked routine, or clear it down. We must also ensure that we restore the higher level routine on exit, which forces us to have a clearup function even if none were otherwise needed. Even in the error return case there is an additional problem. The system defined error numbers may not include a value which describes the real cause of the problem as far as the higher levels are concerned. Either the routine wishes to return an error which relates to some internal factor, so the system has not been involved at all, or just the system error is not really enough information about the problem, whereas a dump and exit is too extreme. We need to be able to report not just the original error if any, but also additional values reflecting the particular routine. Furthermore, any error numbers should ideally be specific to the routine and not general. This obsession with supplying information has a further pitfall. By the time we return to top level, an error may have been passed up by several levels of intermediate routine. If we end up with several different error values, how is the program to make sense of them in a way which allows it selectively to ignore some, report others, and fail on the rest. In order to ease this problem, it is useful to identify various error classes, which describe what sort of thing happened, without trying to be too specific as to details. If these classes can be chosen well, it should only be necessary to examine the class of the original error to decide how to act, and a more detailed examination of the error numbers becomes exceptional. So, where have we got to? Currently, we have identified two reasonable ways of dealing with error information where different routines are involved, user-defined functions and error number(s). We have identified the exception ( possibly implemented as a controlled goto ) as a sensible way of ensuring routine clearup. We have seen that these techniques, even in combination, have a number of undesirable features. In the next section, I discuss a few reasonable additions to languages which might help to solve the problem. The idealised solution requires language support In the section on exception handlers, we mentioned that some languages, including incidentally many versions of FORTRAN, provide a method of passing exceptions or labels as parameters to routines. This allows the routine, if it fails, to cause an exception at the level above, in addition to setting an error number or whatever. FORTRAN goes one better, by making the routines treat the appropriate class of error as fatal if the exception label is omitted. What it doesn't do is allow the user to write such code in their own routines. What we really want is a slight extension of this idea. When calling a routine, one should be able to do so in one of three ways, each syntactically easy to code. Firstly, one should be able to specify a global action whenever an unhandled error occurs. This action should automatically stack with the calling routine, so that it can be overridden at a lower level without affecting higher levels. Secondly, one should be able to specify a label or exception to go to if the routine fails. Thirdly, one can require that the routine on failure should return control as normal. Fourthly, there should be built-in support for specifying the return value of the routine without yet returning, and for setting up a return value, and going to the clearup code. In all cases, a stack of error numbers which give a list of errors from the bottom level up should be availabe to examine, along with an error class for control flow decision making. Whenever a routine fails, the caller must stack a code of its own to indicate the routine through which the error was passed. It may, alternatively, replace the entire stack with a new one of a different class or description if appropriate. Errors in error handlers. At this point, you might think that we have just about exhausted the issue. There is, however, one remaining rather nasty issue. What happens if, while clearing up as a result of an error, one of our clearup routines itself fails. As always there are several possibilities: - Ignore the new error completely, and ensure that such failure have no untoward results. - Treat the new error as unconditionally fatal - Be able to supply a special emergency error handler for failures during clearup ( etc etc ? ) We can reject the second solution as we rejected unconditional fatal errors before. The third solution is workable, and the most general, but requires the ability to save the original error stack and use a new one for handling the current level of clearup, and so on and so on. In practice, the first solution turns out to be the best under normal circumstances. Typically, clearup routines only fail if the data they were handling is now useless, so failing won't make anything worse than it was. On the other hand, later clearup functions may be successful, and it would be a shame to lose the chance to run them. An implementation in C which works in practice Now that we've decided more or less what we want, let's look at a series of routines and macros in C which comes some way towards achieving our goal. Consider the following code struct errorinfo { jmp_buf b; struct errorinfo *prev; }; int globalerror; int clearing_up; struct errorinfo *errorlist; resulttype myproc() { resulttype result; struct errorinfo *ei = malloc(sizeof(struct errorinfo)); ei->prev = errorlist; errorlist = ei; if (setjmp(ei->b) != 0) goto finish; .... here goes the main code ... result = (the final result); finish: clearing_up ++; ... a whole load of clear up routines clearing_up --; errorlist = ei->prev; free(ei); if (globalerror) longjmp(errorlist->b); return result; } If left to its own devices, this routine will simply stack a longjump address so that anything calling the appropriate longjmp will go straight to the finish label in that routine. Suppose that within the code somewhere we have the line: globalerror = x;goto finish; This will have the effect of a controlled jump on a failure event. Furthermore, the routine will exit with a jump to the finish label of its calling routine. This scheme has the tremendous advantage that it stacks properly. In other words, however deep you are, setting globalerror and exiting will take you all the way back to top level, with the finish routines called each time. The purpose of clearing_up is to ensure that if it is non_zero, the routine can tell that an error is already being handled, and treat further failure appropriately. The major disadvantage of the above scheme is that it relies on all routines behaving properly. The consequences of doing a direct return without clearup are rather nasty in terms of future behaviour on errors. However C++ gives us the chance to avoid these problems. If we put the handling of errorlist into constructors and destructors for an error class, we can ensure that the destructor is called however we exit. At the same time we can allow a number of other improvements to the simple scheme above. We can maintain a stack of errors as well as an error class, which allows us to identify precisely where an error occurs. We can use macros to hide the workings from the user. We can also allow a flag at each level to disable the automatic jump, allowing us to intercept lower level errors and reset or alter them. As a result, we end up with the following implementation. ///////////////////// errors.h /////////////////////////////// #define MAXNUERRORS 10 #define MAXNAMELEN 30 extern int n_u_errors; extern int clearing_up; extern int32 u_error[MAXNUERRORS]; extern int32 errorclass; extern char u_name[MAXNUERRORS][MAXNAMELEN+1]; struct ui_error_handler { public: void onerror(int n= -1); void pusherror(int32); void reseterror(); void report_system_error(char *name); ui_error_handler(char *c); ~ui_error_handler(); int next_error_return_val; jmp_buf jumpto; private: int explicit_error; char *name; ui_error_handler *prev; int32 mask; }; #define EC_TEMPRES 1 #define EC_INVALID 2 #define EC_PERM 3 #define EC_STATE 4 #define EC_CORRUPT 5 #define EC_INTERRUPT 6 #define EC_HARDWARE 7 #define EC_INTERNAL 8 #define STARTDEFS(name) ui_error_handler eh(name) #define ENDDEFS {if (setjmp(eh.jumpto)) goto finish;} #define STARTCLEARUP finish: clearing_up++ #define ENDCLEARUP (clearing_up--) #define WASERROR ((n_u_errors > 0) && (! clearing_up)) #define HADERROR (WASERROR && (eh.reseterror(),1)) #define PUSHERROR(errnum) { if (!clearing_up) eh.pusherror(errnum);} #define IFFAIL(errnum,i) {if WASERROR {eh.pusherror(errnum);result = (i);goto finish;}} #define FAIL(i) {if WASERROR {result = (i);goto finish;}} #define NEXTERROR(i) ( eh.next_error_return_val = i ) #define CLEARUP {goto finish;} #define RETURNERROR(class,errnum,s,i) { if (!clearing_up) {eh.reseterror();eh.pusherror(errnum);errorclass = class;result = (i);goto finish;} } #define NORETURNERROR(class,errnum,s) { if (!clearing_up) {eh.reseterror();eh.pusherror(errnum);errorclass = class;} } #define RESETERROR eh.reseterror() #define ONERROR(n) eh.onerror(n) #define SYSTEMERROR(name) eh.report_system_error(name) /////////////////////// errrors.cc //////////////////////////// #include < ... various operating system files ... > #include <errors.h> extern int errno; int n_u_errors; int clearing_up; int next_error_return_val; int32 errorclass; int32 u_error[MAXNUERRORS]; char u_name[MAXNUERRORS][MAXNAMELEN+1]; jmp_buf global_jump_buf_list[20]; int global_jump_buf_level; int32 global_jump_buf_mask[20]; int (*default_error_proc)(); static ui_error_handler *top_errorhandler = 0; ui_error_handler::ui_error_handler(char *c) { mask = 0; if (n_u_errors && (clearing_up == 0)) { longjmp(top_errorhandler->jumpto,1); } name = c; prev = top_errorhandler; top_errorhandler = this; next_error_return_val = 2000; explicit_error = 0; if (clearing_up == 0) n_u_errors = 0; } ui_error_handler::~ui_error_handler() { top_errorhandler = prev; if (prev == 0) return; if ( (clearing_up==0) && n_u_errors) { if(!explicit_error) { char *dest,*n; n_u_errors++; u_error[n_u_errors-1] = next_error_return_val; dest = u_name[n_u_errors-1]; n = name; while(*n) *(dest++) = *(n++); *dest = 0; } if (prev->mask) longjmp(prev->jumpto,1); } }; void ui_error_handler::onerror(int n) { mask = n; }; void ui_error_handler::reseterror() { n_u_errors = 0; clearing_up = 0; } void ui_error_handler::pusherror(int32 errnum) { char *dest,*n; explicit_error = 1; n_u_errors++; u_error[n_u_errors-1] = errnum; dest = u_name[n_u_errors-1]; n = name; while(*n) *(dest++) = *(n++); *dest = 0; } char *description_corresponding_to_errorclass(int errorclass) { switch (errorclass) { case EC_TEMPRES: return "Temp resource problem"; case EC_INVALID: return "Invalid request"; case EC_PERM: return "Permission problem"; case EC_STATE: return "Resource in inappropriate state"; case EC_CORRUPT: return "Resource corrupted"; case EC_INTERRUPT: return "Call interrupted"; case EC_HARDWARE: return "Hardware/external problem"; case EC_INTERNAL: return "Internal error or bug"; default: return "Unknown problem"; } } class report_error_on_exit { public: report_error_on_exit() {}; ~report_error_on_exit(); }; report_error_on_exit::~report_error_on_exit() { int i; if (n_u_errors) { cerr << "\n ** main program exited with the following errors **\n" << " ** with error class: " << description_corresponding_to_errorclass(errorclass) << " **\n"; for(int i = 0; i < n_u_errors; i++) cerr << u_error[i] << " in " << u_name[i] << "\n"; cerr << "\n"; } } static class report_error_on_exit exreport; void ui_error_handler::report_system_error(char *name) { int ec = EC_INVALID; reseterror(); switch (errno) { case 1: case 13: case 30: case 27: ec= EC_PERM; break; case 11: case 12: case 16: case 23: case 24: case 26: case 28: case 31: case 36: case 37: case 72: case 67: case 68: case 69: case 55: case 59: case 35: case 73: case 74: ec=EC_TEMPRES; break; case 9: case 56: case 57: case 58: ec=EC_STATE; break; case 32: case 75: case 70: case 53: ec = EC_CORRUPT; break; case 4: ec= EC_INTERRUPT; break; case 60: case 64: case 65: case 52: case 50: case 5: ec = EC_HARDWARE; break; } char *dest,*n; n_u_errors++; u_error[n_u_errors-1] = errno; dest = u_name[n_u_errors-1]; n = name; while(*n) *(dest++) = *(n++); *dest = 0; errorclass = ec; } Conclusion As we have seen, it is possible, although not nicely, to meet most of our objectives within the existing framework of C++. The system above has been in succesful day to day use on at least one significant project, and has proved its worth. However, it is by no means a perfect solution, and the author would be very pleased to receive suggestions for improvements, and also for consistent modifications to the specifications of C++ or other languages to build these features in. HERE ENDS THE DOCUMENT -- Regards, Kers 24059 | "You're better off not dreaming of the things to come; Caravan: | Dreams are always ending far too soon."