DANE: smtp transport option dane_require_tls_ciphers
[exim.git] / src / src / dbfn.c
CommitLineData
059ec3d9
PH
1/*************************************************
2* Exim - an Internet mail transport agent *
3*************************************************/
4
f9ba5e22 5/* Copyright (c) University of Cambridge 1995 - 2018 */
059ec3d9
PH
6/* See the file NOTICE for conditions of use and distribution. */
7
8
9#include "exim.h"
10
11
12/* Functions for accessing Exim's hints database, which consists of a number of
13different DBM files. This module does not contain code for reading DBM files
14for (e.g.) alias expansion. That is all contained within the general search
15functions. As Exim now has support for several DBM interfaces, all the relevant
16functions are called as macros.
17
18All the data in Exim's database is in the nature of *hints*. Therefore it
19doesn't matter if it gets destroyed by accident. These functions are not
20supposed to implement a "safe" database.
21
22Keys are passed in as C strings, and the terminating zero *is* used when
23building the dbm files. This just makes life easier when scanning the files
24sequentially.
25
26Synchronization is required on the database files, and this is achieved by
27means of locking on independent lock files. (Earlier attempts to lock on the
28DBM files themselves were never completely successful.) Since callers may in
29general want to do more than one read or write while holding the lock, there
30are separate open and close functions. However, the calling modules should
31arrange to hold the locks for the bare minimum of time. */
32
33
34
35/*************************************************
36* Berkeley DB error callback *
37*************************************************/
38
39/* For Berkeley DB >= 2, we can define a function to be called in case of DB
40errors. This should help with debugging strange DB problems, e.g. getting "File
1f922db1
PH
41exists" when you try to open a db file. The API for this function was changed
42at DB release 4.3. */
059ec3d9
PH
43
44#if defined(USE_DB) && defined(DB_VERSION_STRING)
45void
1f922db1
PH
46#if DB_VERSION_MAJOR > 4 || (DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 3)
47dbfn_bdb_error_callback(const DB_ENV *dbenv, const char *pfx, const char *msg)
48{
49dbenv = dbenv;
50#else
059ec3d9
PH
51dbfn_bdb_error_callback(const char *pfx, char *msg)
52{
1f922db1 53#endif
059ec3d9
PH
54pfx = pfx;
55log_write(0, LOG_MAIN, "Berkeley DB error: %s", msg);
56}
57#endif
58
59
60
61
62/*************************************************
63* Open and lock a database file *
64*************************************************/
65
66/* Used for accessing Exim's hints databases.
67
68Arguments:
69 name The single-component name of one of Exim's database files.
70 flags Either O_RDONLY or O_RDWR, indicating the type of open required;
71 O_RDWR implies "create if necessary"
72 dbblock Points to an open_db block to be filled in.
73 lof If TRUE, write to the log for actual open failures (locking failures
74 are always logged).
75
76Returns: NULL if the open failed, or the locking failed. After locking
77 failures, errno is zero.
78
79 On success, dbblock is returned. This contains the dbm pointer and
80 the fd of the locked lock file.
eff37e47
PH
81
82There are some calls that use O_RDWR|O_CREAT for the flags. Having discovered
83this in December 2005, I'm not sure if this is correct or not, but for the
84moment I haven't changed them.
059ec3d9
PH
85*/
86
87open_db *
88dbfn_open(uschar *name, int flags, open_db *dbblock, BOOL lof)
89{
90int rc, save_errno;
91BOOL read_only = flags == O_RDONLY;
92BOOL created = FALSE;
93flock_t lock_data;
cfb9cf20 94uschar dirname[256], filename[256];
059ec3d9
PH
95
96/* The first thing to do is to open a separate file on which to lock. This
97ensures that Exim has exclusive use of the database before it even tries to
98open it. Early versions tried to lock on the open database itself, but that
99gave rise to mysterious problems from time to time - it was suspected that some
100DB libraries "do things" on their open() calls which break the interlocking.
101The lock file is never written to, but we open it for writing so we can get a
102write lock if required. If it does not exist, we create it. This is done
103separately so we know when we have done it, because when running as root we
104need to change the ownership - see the bottom of this function. We also try to
105make the directory as well, just in case. We won't be doing this many times
106unnecessarily, because usually the lock file will be there. If the directory
107exists, there is no error. */
108
cfb9cf20
JH
109snprintf(CS dirname, sizeof(dirname), "%s/db", spool_directory);
110snprintf(CS filename, sizeof(filename), "%s/%s.lockfile", dirname, name);
059ec3d9 111
cfb9cf20 112if ((dbblock->lockfd = Uopen(filename, O_RDWR, EXIMDB_LOCKFILE_MODE)) < 0)
059ec3d9
PH
113 {
114 created = TRUE;
115 (void)directory_make(spool_directory, US"db", EXIMDB_DIRECTORY_MODE, TRUE);
cfb9cf20 116 dbblock->lockfd = Uopen(filename, O_RDWR|O_CREAT, EXIMDB_LOCKFILE_MODE);
059ec3d9
PH
117 }
118
119if (dbblock->lockfd < 0)
120 {
121 log_write(0, LOG_MAIN, "%s",
cfb9cf20 122 string_open_failed(errno, "database lock file %s", filename));
059ec3d9
PH
123 errno = 0; /* Indicates locking failure */
124 return NULL;
125 }
126
127/* Now we must get a lock on the opened lock file; do this with a blocking
128lock that times out. */
129
130lock_data.l_type = read_only? F_RDLCK : F_WRLCK;
131lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0;
132
133DEBUG(D_hints_lookup|D_retry|D_route|D_deliver)
cfb9cf20 134 debug_printf("locking %s\n", filename);
059ec3d9
PH
135
136sigalrm_seen = FALSE;
137alarm(EXIMDB_LOCK_TIMEOUT);
138rc = fcntl(dbblock->lockfd, F_SETLKW, &lock_data);
139alarm(0);
140
141if (sigalrm_seen) errno = ETIMEDOUT;
142if (rc < 0)
143 {
8c07b69f 144 log_write(0, LOG_MAIN|LOG_PANIC, "Failed to get %s lock for %s: %s",
cfb9cf20 145 read_only ? "read" : "write", filename,
7b4c8c1f 146 errno == ETIMEDOUT ? "timed out" : strerror(errno));
f1e894f3 147 (void)close(dbblock->lockfd);
059ec3d9
PH
148 errno = 0; /* Indicates locking failure */
149 return NULL;
150 }
151
cfb9cf20 152DEBUG(D_hints_lookup) debug_printf("locked %s\n", filename);
059ec3d9
PH
153
154/* At this point we have an opened and locked separate lock file, that is,
155exclusive access to the database, so we can go ahead and open it. If we are
156expected to create it, don't do so at first, again so that we can detect
157whether we need to change its ownership (see comments about the lock file
8187c3f3
PH
158above.) There have been regular reports of crashes while opening hints
159databases - often this is caused by non-matching db.h and the library. To make
160it easy to pin this down, there are now debug statements on either side of the
161open call. */
059ec3d9 162
cfb9cf20 163snprintf(CS filename, sizeof(filename), "%s/%s", dirname, name);
cfb9cf20 164EXIM_DBOPEN(filename, dirname, flags, EXIMDB_MODE, &(dbblock->dbptr));
059ec3d9 165
7b4c8c1f 166if (!dbblock->dbptr && errno == ENOENT && flags == O_RDWR)
059ec3d9
PH
167 {
168 DEBUG(D_hints_lookup)
cfb9cf20 169 debug_printf("%s appears not to exist: trying to create\n", filename);
059ec3d9 170 created = TRUE;
cfb9cf20 171 EXIM_DBOPEN(filename, dirname, flags|O_CREAT, EXIMDB_MODE, &(dbblock->dbptr));
059ec3d9
PH
172 }
173
174save_errno = errno;
175
176/* If we are running as root and this is the first access to the database, its
177files will be owned by root. We want them to be owned by exim. We detect this
178situation by noting above when we had to create the lock file or the database
179itself. Because the different dbm libraries use different extensions for their
180files, I don't know of any easier way of arranging this than scanning the
181directory for files with the appropriate base name. At least this deals with
182the lock file at the same time. Also, the directory will typically have only
183half a dozen files, so the scan will be quick.
184
185This code is placed here, before the test for successful opening, because there
186was a case when a file was created, but the DBM library still returned NULL
187because of some problem. It also sorts out the lock file if that was created
188but creation of the database file failed. */
189
190if (created && geteuid() == root_uid)
191 {
192 DIR *dd;
193 struct dirent *ent;
cfb9cf20 194 uschar *lastname = Ustrrchr(filename, '/') + 1;
059ec3d9
PH
195 int namelen = Ustrlen(name);
196
197 *lastname = 0;
cfb9cf20 198 dd = opendir(CS filename);
059ec3d9 199
7b4c8c1f 200 while ((ent = readdir(dd)))
059ec3d9
PH
201 if (Ustrncmp(ent->d_name, name, namelen) == 0)
202 {
203 struct stat statbuf;
204 Ustrcpy(lastname, ent->d_name);
cfb9cf20 205 if (Ustat(filename, &statbuf) >= 0 && statbuf.st_uid != exim_uid)
059ec3d9 206 {
cfb9cf20
JH
207 DEBUG(D_hints_lookup) debug_printf("ensuring %s is owned by exim\n", filename);
208 if (Uchown(filename, exim_uid, exim_gid))
209 DEBUG(D_hints_lookup) debug_printf("failed setting %s to owned by exim\n", filename);
059ec3d9
PH
210 }
211 }
059ec3d9
PH
212
213 closedir(dd);
214 }
215
216/* If the open has failed, return NULL, leaving errno set. If lof is TRUE,
0a6c178c
JH
217log the event - also for debugging - but debug only if the file just doesn't
218exist. */
059ec3d9 219
7b4c8c1f 220if (!dbblock->dbptr)
059ec3d9 221 {
0a6c178c
JH
222 if (lof && save_errno != ENOENT)
223 log_write(0, LOG_MAIN, "%s", string_open_failed(save_errno, "DB file %s",
cfb9cf20 224 filename));
0a6c178c
JH
225 else
226 DEBUG(D_hints_lookup)
227 debug_printf("%s\n", CS string_open_failed(save_errno, "DB file %s",
cfb9cf20 228 filename));
f1e894f3 229 (void)close(dbblock->lockfd);
059ec3d9
PH
230 errno = save_errno;
231 return NULL;
232 }
233
234DEBUG(D_hints_lookup)
cfb9cf20 235 debug_printf("opened hints database %s: flags=%s\n", filename,
7b4c8c1f
JH
236 flags == O_RDONLY ? "O_RDONLY"
237 : flags == O_RDWR ? "O_RDWR"
238 : flags == (O_RDWR|O_CREAT) ? "O_RDWR|O_CREAT"
239 : "??");
059ec3d9
PH
240
241/* Pass back the block containing the opened database handle and the open fd
242for the lock. */
243
244return dbblock;
245}
246
247
248
249
250/*************************************************
251* Unlock and close a database file *
252*************************************************/
253
254/* Closing a file automatically unlocks it, so after closing the database, just
255close the lock file.
256
257Argument: a pointer to an open database block
258Returns: nothing
259*/
260
261void
262dbfn_close(open_db *dbblock)
263{
264EXIM_DBCLOSE(dbblock->dbptr);
f1e894f3 265(void)close(dbblock->lockfd);
7b4c8c1f 266DEBUG(D_hints_lookup) debug_printf("closed hints database and lockfile\n");
059ec3d9
PH
267}
268
269
270
271
272/*************************************************
273* Read from database file *
274*************************************************/
275
276/* Passing back the pointer unchanged is useless, because there is
277no guarantee of alignment. Since all the records used by Exim need
278to be properly aligned to pick out the timestamps, etc., we might as
279well do the copying centrally here.
280
281Most calls don't need the length, so there is a macro called dbfn_read which
282has two arguments; it calls this function adding NULL as the third.
283
284Arguments:
285 dbblock a pointer to an open database block
286 key the key of the record to be read
287 length a pointer to an int into which to return the length, if not NULL
288
289Returns: a pointer to the retrieved record, or
290 NULL if the record is not found
291*/
292
293void *
55414b25 294dbfn_read_with_length(open_db *dbblock, const uschar *key, int *length)
059ec3d9
PH
295{
296void *yield;
297EXIM_DATUM key_datum, result_datum;
55414b25
JH
298int klen = Ustrlen(key) + 1;
299uschar * key_copy = store_get(klen);
300
301memcpy(key_copy, key, klen);
059ec3d9
PH
302
303DEBUG(D_hints_lookup) debug_printf("dbfn_read: key=%s\n", key);
304
305EXIM_DATUM_INIT(key_datum); /* Some DBM libraries require the datum */
306EXIM_DATUM_INIT(result_datum); /* to be cleared before use. */
55414b25
JH
307EXIM_DATUM_DATA(key_datum) = CS key_copy;
308EXIM_DATUM_SIZE(key_datum) = klen;
059ec3d9
PH
309
310if (!EXIM_DBGET(dbblock->dbptr, key_datum, result_datum)) return NULL;
311
312yield = store_get(EXIM_DATUM_SIZE(result_datum));
313memcpy(yield, EXIM_DATUM_DATA(result_datum), EXIM_DATUM_SIZE(result_datum));
314if (length != NULL) *length = EXIM_DATUM_SIZE(result_datum);
315
316EXIM_DATUM_FREE(result_datum); /* Some DBM libs require freeing */
317return yield;
318}
319
320
321
322/*************************************************
323* Write to database file *
324*************************************************/
325
326/*
327Arguments:
328 dbblock a pointer to an open database block
329 key the key of the record to be written
330 ptr a pointer to the record to be written
331 length the length of the record to be written
332
333Returns: the yield of the underlying dbm or db "write" function. If this
334 is dbm, the value is zero for OK.
335*/
336
337int
55414b25 338dbfn_write(open_db *dbblock, const uschar *key, void *ptr, int length)
059ec3d9
PH
339{
340EXIM_DATUM key_datum, value_datum;
341dbdata_generic *gptr = (dbdata_generic *)ptr;
55414b25
JH
342int klen = Ustrlen(key) + 1;
343uschar * key_copy = store_get(klen);
344
345memcpy(key_copy, key, klen);
059ec3d9
PH
346gptr->time_stamp = time(NULL);
347
348DEBUG(D_hints_lookup) debug_printf("dbfn_write: key=%s\n", key);
349
350EXIM_DATUM_INIT(key_datum); /* Some DBM libraries require the datum */
351EXIM_DATUM_INIT(value_datum); /* to be cleared before use. */
55414b25
JH
352EXIM_DATUM_DATA(key_datum) = CS key_copy;
353EXIM_DATUM_SIZE(key_datum) = klen;
059ec3d9
PH
354EXIM_DATUM_DATA(value_datum) = CS ptr;
355EXIM_DATUM_SIZE(value_datum) = length;
356return EXIM_DBPUT(dbblock->dbptr, key_datum, value_datum);
357}
358
359
360
361/*************************************************
362* Delete record from database file *
363*************************************************/
364
365/*
366Arguments:
367 dbblock a pointer to an open database block
368 key the key of the record to be deleted
369
370Returns: the yield of the underlying dbm or db "delete" function.
371*/
372
373int
55414b25 374dbfn_delete(open_db *dbblock, const uschar *key)
059ec3d9 375{
55414b25
JH
376int klen = Ustrlen(key) + 1;
377uschar * key_copy = store_get(klen);
378
379memcpy(key_copy, key, klen);
059ec3d9
PH
380EXIM_DATUM key_datum;
381EXIM_DATUM_INIT(key_datum); /* Some DBM libraries require clearing */
55414b25
JH
382EXIM_DATUM_DATA(key_datum) = CS key_copy;
383EXIM_DATUM_SIZE(key_datum) = klen;
059ec3d9
PH
384return EXIM_DBDEL(dbblock->dbptr, key_datum);
385}
386
387
388
389/*************************************************
390* Scan the keys of a database file *
391*************************************************/
392
393/*
394Arguments:
395 dbblock a pointer to an open database block
396 start TRUE if starting a new scan
397 FALSE if continuing with the current scan
398 cursor a pointer to a pointer to a cursor anchor, for those dbm libraries
399 that use the notion of a cursor
400
401Returns: the next record from the file, or
402 NULL if there are no more
403*/
404
405uschar *
406dbfn_scan(open_db *dbblock, BOOL start, EXIM_CURSOR **cursor)
407{
408EXIM_DATUM key_datum, value_datum;
409uschar *yield;
410value_datum = value_datum; /* dummy; not all db libraries use this */
411
412/* Some dbm require an initialization */
413
414if (start) EXIM_DBCREATE_CURSOR(dbblock->dbptr, cursor);
415
416EXIM_DATUM_INIT(key_datum); /* Some DBM libraries require the datum */
417EXIM_DATUM_INIT(value_datum); /* to be cleared before use. */
418
419yield = (EXIM_DBSCAN(dbblock->dbptr, key_datum, value_datum, start, *cursor))?
420 US EXIM_DATUM_DATA(key_datum) : NULL;
421
422/* Some dbm require a termination */
423
424if (!yield) EXIM_DBDELETE_CURSOR(*cursor);
425return yield;
426}
427
428
429
430/*************************************************
431**************************************************
432* Stand-alone test program *
433**************************************************
434*************************************************/
435
436#ifdef STAND_ALONE
437
438int
439main(int argc, char **cargv)
440{
441open_db dbblock[8];
442int max_db = sizeof(dbblock)/sizeof(open_db);
443int current = -1;
444int showtime = 0;
445int i;
446dbdata_wait *dbwait = NULL;
447uschar **argv = USS cargv;
448uschar buffer[256];
449uschar structbuffer[1024];
450
451if (argc != 2)
452 {
453 printf("Usage: test_dbfn directory\n");
454 printf("The subdirectory called \"db\" in the given directory is used for\n");
455 printf("the files used in this test program.\n");
456 return 1;
457 }
458
459/* Initialize */
460
461spool_directory = argv[1];
462debug_selector = D_all - D_memory;
463debug_file = stderr;
464big_buffer = malloc(big_buffer_size);
465
466for (i = 0; i < max_db; i++) dbblock[i].dbptr = NULL;
467
468printf("\nExim's db functions tester: interface type is %s\n", EXIM_DBTYPE);
469printf("DBM library: ");
470
471#ifdef DB_VERSION_STRING
472printf("Berkeley DB: %s\n", DB_VERSION_STRING);
473#elif defined(BTREEVERSION) && defined(HASHVERSION)
474 #ifdef USE_DB
475 printf("probably Berkeley DB version 1.8x (native mode)\n");
476 #else
477 printf("probably Berkeley DB version 1.8x (compatibility mode)\n");
478 #endif
479#elif defined(_DBM_RDONLY) || defined(dbm_dirfno)
480printf("probably ndbm\n");
481#elif defined(USE_TDB)
482printf("using tdb\n");
483#else
484 #ifdef USE_GDBM
485 printf("probably GDBM (native mode)\n");
486 #else
487 printf("probably GDBM (compatibility mode)\n");
488 #endif
489#endif
490
491/* Test the functions */
492
493printf("\nTest the functions\n> ");
494
495while (Ufgets(buffer, 256, stdin) != NULL)
496 {
497 int len = Ustrlen(buffer);
498 int count = 1;
499 clock_t start = 1;
500 clock_t stop = 0;
501 uschar *cmd = buffer;
502 while (len > 0 && isspace((uschar)buffer[len-1])) len--;
503 buffer[len] = 0;
504
505 if (isdigit((uschar)*cmd))
506 {
507 count = Uatoi(cmd);
508 while (isdigit((uschar)*cmd)) cmd++;
509 while (isspace((uschar)*cmd)) cmd++;
510 }
511
512 if (Ustrncmp(cmd, "open", 4) == 0)
513 {
514 int i;
515 open_db *odb;
516 uschar *s = cmd + 4;
517 while (isspace((uschar)*s)) s++;
518
519 for (i = 0; i < max_db; i++)
520 if (dbblock[i].dbptr == NULL) break;
521
522 if (i >= max_db)
523 {
524 printf("Too many open databases\n> ");
525 continue;
526 }
527
528 start = clock();
529 odb = dbfn_open(s, O_RDWR, dbblock + i, TRUE);
530 stop = clock();
531
0a6c178c 532 if (odb)
059ec3d9
PH
533 {
534 current = i;
535 printf("opened %d\n", current);
536 }
537 /* Other error cases will have written messages */
538 else if (errno == ENOENT)
539 {
540 printf("open failed: %s%s\n", strerror(errno),
541 #ifdef USE_DB
542 " (or other Berkeley DB error)"
543 #else
544 ""
545 #endif
546 );
547 }
548 }
549
550 else if (Ustrncmp(cmd, "write", 5) == 0)
551 {
552 int rc = 0;
553 uschar *key = cmd + 5;
554 uschar *data;
555
556 if (current < 0)
557 {
558 printf("No current database\n");
559 continue;
560 }
561
562 while (isspace((uschar)*key)) key++;
563 data = key;
564 while (*data != 0 && !isspace((uschar)*data)) data++;
565 *data++ = 0;
566 while (isspace((uschar)*data)) data++;
567
568 dbwait = (dbdata_wait *)(&structbuffer);
569 Ustrcpy(dbwait->text, data);
570
571 start = clock();
572 while (count-- > 0)
573 rc = dbfn_write(dbblock + current, key, dbwait,
574 Ustrlen(data) + sizeof(dbdata_wait));
575 stop = clock();
576 if (rc != 0) printf("Failed: %s\n", strerror(errno));
577 }
578
579 else if (Ustrncmp(cmd, "read", 4) == 0)
580 {
581 uschar *key = cmd + 4;
582 if (current < 0)
583 {
584 printf("No current database\n");
585 continue;
586 }
587 while (isspace((uschar)*key)) key++;
588 start = clock();
589 while (count-- > 0)
590 dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock+ current, key, NULL);
591 stop = clock();
592 printf("%s\n", (dbwait == NULL)? "<not found>" : CS dbwait->text);
593 }
594
595 else if (Ustrncmp(cmd, "delete", 6) == 0)
596 {
597 uschar *key = cmd + 6;
598 if (current < 0)
599 {
600 printf("No current database\n");
601 continue;
602 }
603 while (isspace((uschar)*key)) key++;
604 dbfn_delete(dbblock + current, key);
605 }
606
607 else if (Ustrncmp(cmd, "scan", 4) == 0)
608 {
609 EXIM_CURSOR *cursor;
610 BOOL startflag = TRUE;
611 uschar *key;
612 uschar keybuffer[256];
613 if (current < 0)
614 {
615 printf("No current database\n");
616 continue;
617 }
618 start = clock();
619 while ((key = dbfn_scan(dbblock + current, startflag, &cursor)) != NULL)
620 {
621 startflag = FALSE;
622 Ustrcpy(keybuffer, key);
623 dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock + current,
624 keybuffer, NULL);
625 printf("%s: %s\n", keybuffer, dbwait->text);
626 }
627 stop = clock();
628 printf("End of scan\n");
629 }
630
631 else if (Ustrncmp(cmd, "close", 5) == 0)
632 {
633 uschar *s = cmd + 5;
634 while (isspace((uschar)*s)) s++;
635 i = Uatoi(s);
636 if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); else
637 {
638 start = clock();
639 dbfn_close(dbblock + i);
640 stop = clock();
641 dbblock[i].dbptr = NULL;
642 if (i == current) current = -1;
643 }
644 }
645
646 else if (Ustrncmp(cmd, "file", 4) == 0)
647 {
648 uschar *s = cmd + 4;
649 while (isspace((uschar)*s)) s++;
650 i = Uatoi(s);
651 if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n");
652 else current = i;
653 }
654
655 else if (Ustrncmp(cmd, "time", 4) == 0)
656 {
657 showtime = ~showtime;
658 printf("Timing %s\n", showtime? "on" : "off");
659 }
660
661 else if (Ustrcmp(cmd, "q") == 0 || Ustrncmp(cmd, "quit", 4) == 0) break;
662
663 else if (Ustrncmp(cmd, "help", 4) == 0)
664 {
665 printf("close [<number>] close file [<number>]\n");
666 printf("delete <key> remove record from current file\n");
667 printf("file <number> make file <number> current\n");
668 printf("open <name> open db file\n");
669 printf("q[uit] exit program\n");
670 printf("read <key> read record from current file\n");
671 printf("scan scan current file\n");
672 printf("time time display on/off\n");
673 printf("write <key> <rest-of-line> write record to current file\n");
674 }
675
676 else printf("Eh?\n");
677
678 if (showtime && stop >= start)
679 printf("start=%d stop=%d difference=%d\n", (int)start, (int)stop,
680 (int)(stop - start));
681
682 printf("> ");
683 }
684
685for (i = 0; i < max_db; i++)
686 {
687 if (dbblock[i].dbptr != NULL)
688 {
689 printf("\nClosing %d", i);
690 dbfn_close(dbblock + i);
691 }
692 }
693
694printf("\n");
695return 0;
696}
697
698#endif
699
700/* End of dbfn.c */