]> git.sur5r.net Git - bacula/bacula/blob - bacula/src/stored/faketape.c
ee98d31cc7d960dda80427dc817c88dcf2e307b4
[bacula/bacula] / bacula / src / stored / faketape.c
1 /*
2    Bacula® - The Network Backup Solution
3
4    Copyright (C) 2000-2008 Free Software Foundation Europe e.V.
5
6    The main author of Bacula is Kern Sibbald, with contributions from
7    many others, a complete list can be found in the file AUTHORS.
8    This program is Free Software; you can redistribute it and/or
9    modify it under the terms of version two of the GNU General Public
10    License as published by the Free Software Foundation, which is 
11    listed in the file LICENSE.
12
13    This program is distributed in the hope that it will be useful, but
14    WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16    General Public License for more details.
17
18    You should have received a copy of the GNU General Public License
19    along with this program; if not, write to the Free Software
20    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21    02110-1301, USA.
22
23    Bacula® is a registered trademark of John Walker.
24    The licensor of Bacula is the Free Software Foundation Europe
25    (FSFE), Fiduciary Program, Sumatrastrasse 25, 8006 Zürich,
26    Switzerland, email:ftf@fsfeurope.org.
27 */
28
29 /*
30
31 Device {
32   Name = Drive-1                      #
33   Maximum File Size = 800M
34   Maximum Volume Size = 3G
35   Device Type = TAPE
36   Archive Device = /tmp/fake
37   Media Type = DLT-8000
38   AutomaticMount = yes;               # when device opened, read it
39   AlwaysOpen = yes;
40   RemovableMedia = yes;
41   RandomAccess = no;
42 }
43
44   Block description :
45
46   block {
47     int32  size;
48     void   *data;
49   }
50
51   EOF description :
52
53   EOF {
54     int32  size=0;
55   }
56
57
58  */
59
60 #include "bacula.h"             /* define 64bit file usage */
61 #include "stored.h"
62
63 #ifdef USE_FAKETAPE
64 #include "faketape.h"
65
66 static int dbglevel = 10;
67 #define FILE_OFFSET 30
68 faketape *ftape_list[FTAPE_MAX_DRIVE];
69
70 static faketape *get_tape(int fd)
71 {
72    ASSERT(fd >= 0);
73
74    if (fd >= FTAPE_MAX_DRIVE) {
75       /* error */
76       return NULL;
77    }
78
79    return ftape_list[fd];
80 }
81
82 static bool put_tape(faketape *ftape)
83 {
84    ASSERT(ftape != NULL);
85
86    int fd = ftape->get_fd();
87    if (fd >= FTAPE_MAX_DRIVE) {
88       /* error */
89       return false;
90    }
91    ftape_list[fd] = ftape;
92    return true;
93 }
94
95 void faketape_debug(int level)
96 {
97    dbglevel = level;
98 }
99
100 /****************************************************************/
101 /* theses function will replace open/read/write/close/ioctl
102  * in bacula core
103  */
104 int faketape_open(const char *pathname, int flags, ...)
105 {
106    ASSERT(pathname != NULL);
107
108    int fd;
109    faketape *tape = new faketape();
110    fd = tape->open(pathname, flags);
111    if (fd > 0) {
112       put_tape(tape);
113    }
114    return fd;
115 }
116
117 int faketape_read(int fd, void *buffer, unsigned int count)
118 {
119    faketape *tape = get_tape(fd);
120    ASSERT(tape != NULL);
121    return tape->read(buffer, count);
122 }
123
124 int faketape_write(int fd, const void *buffer, unsigned int count)
125 {
126    faketape *tape = get_tape(fd);
127    ASSERT(tape != NULL);
128    return tape->write(buffer, count);
129 }
130
131 int faketape_close(int fd)
132 {
133    faketape *tape = get_tape(fd);
134    ASSERT(tape != NULL);
135    tape->close();
136    delete tape;
137    return 0;
138 }
139
140 int faketape_ioctl(int fd, unsigned long int request, ...)
141 {
142    va_list argp;
143    int result=0;
144
145    faketape *t = get_tape(fd);
146    if (!t) {
147       errno = EBADF;
148       return -1;
149    }
150
151    va_start(argp, request);
152
153    if (request == MTIOCTOP) {
154       result = t->tape_op(va_arg(argp, mtop *));
155    } else if (request == MTIOCGET) {
156       result = t->tape_get(va_arg(argp, mtget *));
157    } else if (request == MTIOCPOS) {
158       result = t->tape_pos(va_arg(argp, mtpos *));
159    } else {
160       errno = ENOTTY;
161       result = -1;
162    }
163    va_end(argp);
164
165    return result;
166 }
167
168 /****************************************************************/
169
170 int faketape::tape_op(struct mtop *mt_com)
171 {
172    int result=0;
173
174    if (!online) {
175       errno = ENOMEDIUM;
176       return -1;
177    }
178    
179    switch (mt_com->mt_op)
180    {
181    case MTRESET:
182    case MTNOP:
183    case MTSETDRVBUFFER:
184       break;
185
186    default:
187    case MTRAS1:
188    case MTRAS2:
189    case MTRAS3:
190    case MTSETDENSITY:
191       errno = ENOTTY;
192       result = -1;
193       break;
194
195    case MTFSF:                  /* Forward space over mt_count filemarks. */
196       result = fsf(mt_com->mt_count);
197       break;
198
199    case MTBSF:                  /* Backward space over mt_count filemarks. */
200       result = bsf(mt_com->mt_count);
201       break;
202
203    case MTFSR:      /* Forward space over mt_count records (tape blocks). */
204 /*
205     file number = 1
206     block number = 0
207    
208     file number = 1
209     block number = 1
210    
211     mt: /dev/lto2: Erreur d'entree/sortie
212    
213     file number = 2
214     block number = 0
215 */
216       /* tester si on se trouve a la fin du fichier */
217       result = fsr(mt_com->mt_count);
218       break;
219
220    case MTBSR:      /* Backward space over mt_count records (tape blocks). */
221       result = bsr(mt_com->mt_count);
222       break;
223
224    case MTWEOF:                 /* Write mt_count filemarks. */
225       result = weof(mt_com->mt_count);
226       break;
227
228    case MTREW:                  /* Rewind. */
229       Dmsg0(dbglevel, "rewind faketape\n");
230       atEOF = atEOD = false;
231       atBOT = true;
232       current_file = 0;
233       current_block = 0;
234       seek_file();
235       break;
236
237    case MTOFFL:                 /* put tape offline */
238       result = offline();
239       break;
240
241    case MTRETEN:                /* Re-tension tape. */
242       result = 0;
243       break;
244
245    case MTBSFM:                 /* not used by bacula */
246       errno = EIO;
247       result = -1;
248       break;
249
250    case MTFSFM:                 /* not used by bacula */
251       errno = EIO;
252       result = -1;
253       break;
254
255    case MTEOM:/* Go to the end of the recorded media (for appending files). */
256 /*
257    file number = 3
258    block number = -1
259 */
260       /* Can be at EOM */
261       atBOT = false;
262       atEOF = false;
263       atEOD = true;
264       atEOT = false;
265
266       current_file = last_file;
267       current_block = -1;
268       seek_file();
269       break;
270
271    case MTERASE:                /* not used by bacula */
272       atEOD = true;
273       atEOF = false;
274       atEOT = false;
275
276       current_file = 0;
277       current_block = -1;
278       seek_file();
279       truncate_file();
280       break;
281
282    case MTSETBLK:
283       break;
284
285    case MTSEEK:
286       break;
287
288    case MTTELL:
289       break;
290
291    case MTFSS:
292       break;
293
294    case MTBSS:
295       break;
296
297    case MTWSM:
298       break;
299
300    case MTLOCK:
301       break;
302
303    case MTUNLOCK:
304       break;
305
306    case MTLOAD:
307       break;
308
309    case MTUNLOAD:
310       break;
311
312    case MTCOMPRESSION:
313       break;
314
315    case MTSETPART:
316       break;
317
318    case MTMKPART:
319       break;
320    }
321
322    return result == 0 ? 0 : -1;
323 }
324
325 int faketape::tape_get(struct mtget *mt_get)
326 {
327    int density = 1;
328    int block_size = 1024;
329
330    mt_get->mt_type = MT_ISSCSI2;
331    mt_get->mt_blkno = current_block;
332    mt_get->mt_fileno = current_file;
333
334    mt_get->mt_resid = -1;
335 //   pos_info.PartitionBlockValid ? pos_info.Partition : (ULONG)-1;
336
337    /* TODO */
338    mt_get->mt_dsreg = 
339       ((density << MT_ST_DENSITY_SHIFT) & MT_ST_DENSITY_MASK) |
340       ((block_size << MT_ST_BLKSIZE_SHIFT) & MT_ST_BLKSIZE_MASK);
341
342
343    mt_get->mt_gstat = 0x00010000;  /* Immediate report mode.*/
344
345    if (atEOF) {
346       mt_get->mt_gstat |= 0x80000000;     // GMT_EOF
347    }
348
349    if (atBOT) {
350       mt_get->mt_gstat |= 0x40000000;     // GMT_BOT
351    }
352    if (atEOT) {
353       mt_get->mt_gstat |= 0x20000000;     // GMT_EOT
354    }
355
356    if (atEOD) {
357       mt_get->mt_gstat |= 0x08000000;     // GMT_EOD
358    }
359
360    if (0) { //WriteProtected) {
361       mt_get->mt_gstat |= 0x04000000;     // GMT_WR_PROT
362    }
363
364    if (online) {
365       mt_get->mt_gstat |= 0x01000000;     // GMT_ONLINE
366    } else {
367       mt_get->mt_gstat |= 0x00040000;  // GMT_DR_OPEN
368    }
369    mt_get->mt_erreg = 0;
370
371    return 0;
372 }
373
374 int faketape::tape_pos(struct mtpos *mt_pos)
375 {
376    if (current_block >= 0) {
377       mt_pos->mt_blkno = current_block;
378       return 0;
379    }
380
381    return -1;
382 }
383
384 /*
385  * This function try to emulate the append only behavior
386  * of a tape. When you wrote something, data after the
387  * current position are discarded.
388  */
389 int faketape::truncate_file()
390 {  
391    Dmsg2(dbglevel, "truncate %i:%i\n", current_file, current_block);
392    ftruncate(fd, lseek(fd, 0, SEEK_CUR));
393    last_file = current_file;
394    atEOD=true;
395    update_pos();
396    return 0;
397 }
398
399 faketape::faketape()
400 {
401    fd = -1;
402
403    atEOF = false;
404    atBOT = false;
405    atEOT = false;
406    atEOD = false;
407    online = false;
408    inplace = false;
409    needEOF = false;
410
411    file_block = 0;
412    last_file = 0;
413    current_file = 0;
414    current_block = -1;
415
416    max_block = 2*1024*2048;      /* 2GB */
417 }
418
419 faketape::~faketape()
420 {
421 }
422
423 int faketape::get_fd()
424 {
425    return this->fd;
426 }
427
428 /*
429  * TODO: check if after a write op, and other tape op put a EOF
430  */
431 int faketape::write(const void *buffer, unsigned int count)
432 {
433    ASSERT(online);
434    ASSERT(current_file >= 0);
435    ASSERT(count > 0);
436    ASSERT(buffer);
437
438    unsigned int nb;
439    Dmsg3(dbglevel, "write len=%i %i:%i\n", count, current_file,current_block);
440
441    if (atEOT) {
442       Dmsg0(dbglevel, "write nothing, EOT !\n");
443       errno = ENOSPC;
444       return -1;
445    }
446
447    check_inplace();
448
449    if (!atEOD) {                /* if not at the end of the data */
450       truncate_file();
451    }
452  
453    if (current_block != -1) {
454       current_block++;
455    }
456
457    atBOT = false;
458    atEOF = false;
459    atEOD = true;                /* End of data */
460
461    needEOF = true;              /* next operation need EOF mark */
462
463 //   if ((count + file_size) > max_size) {
464 //      Dmsg2(dbglevel, 
465 //          "EOT writing only %i of %i requested\n", 
466 //          max_size - file_size, count);
467 //      count = max_size - file_size;
468 //      atEOT = true;
469 //   }
470
471    uint32_t size = count;
472    ::write(fd, &size, sizeof(uint32_t));
473    nb = ::write(fd, buffer, count);
474    
475    if (nb != count) {
476       atEOT = true;
477       Dmsg2(dbglevel, 
478             "Not enough space writing only %i of %i requested\n", 
479             nb, count);
480    }
481
482    update_pos();
483
484    return nb;
485 }
486
487 int faketape::weof(int count)
488 {
489    ASSERT(online);
490    ASSERT(current_file >= 0);
491    Dmsg3(dbglevel, "Writing EOF %i:%i last=%i\n", 
492          current_file, current_block,last_file);
493    if (atEOT) {
494       errno = ENOSPC;
495       current_block = -1;
496       return -1;
497    }
498    needEOF = false;
499
500    check_inplace();
501    truncate_file();             /* nothing after this point */
502
503    uint32_t c=0;
504    ::write(fd, &c, sizeof(uint32_t));
505
506    current_file += count;
507    current_block = 0;
508    seek_file();
509
510    c=0;
511    ::write(fd, &c, sizeof(uint32_t));   
512    lseek(fd, lseek(fd, 0, SEEK_CUR) - sizeof(uint32_t), SEEK_SET);
513
514    atEOD = false;
515    atBOT = false;
516    atEOF = true;
517
518    update_pos();
519
520    return 0;
521 }
522
523 int faketape::fsf(int count)
524 {   
525    ASSERT(online);
526    ASSERT(current_file >= 0);
527    ASSERT(fd >= 0);
528 /*
529  * 1 0 -> fsf -> 2 0 -> fsf -> 2 -1
530  */
531    check_inplace();
532    check_eof();
533
534    int ret;
535    if (atEOT || atEOD) {
536       errno = EIO;
537       current_block = -1;
538       return -1;
539    }
540
541    atBOT = atEOF = false;
542    Dmsg3(dbglevel+1, "fsf %i+%i <= %i\n", current_file, count, last_file);
543
544    if ((current_file + count) <= last_file) {
545       current_file += count;
546       current_block = 0;
547       ret = 0;
548    } else {
549       Dmsg0(dbglevel, "Try to FSF after EOT\n");
550       errno = EIO;
551       current_file = last_file ;
552       current_block = -1;
553       atEOD=true;
554       ret = -1;
555    }
556    seek_file();
557    return ret;
558 }
559
560 int faketape::fsr(int count)
561 {
562    ASSERT(online);
563    ASSERT(current_file >= 0);
564    ASSERT(fd >= 0);
565    
566    int i,nb, ret=0;
567    off_t where=0;
568    uint32_t s;
569    Dmsg3(dbglevel, "fsr %i:%i count=%i\n", current_file,current_block, count);
570
571    check_inplace();
572    check_eof();
573
574    if (atEOT) {
575       errno = EIO;
576       current_block = -1;
577       return -1;
578    }
579
580    if (atEOD) {
581       errno = EIO;
582       return -1;
583    }
584
585    atBOT = atEOF = false;   
586
587    /* check all block record */
588    for(i=0; (i < count) && !atEOF ; i++) {
589       nb = ::read(fd, &s, sizeof(uint32_t)); /* get size of next block */
590       if (nb == sizeof(uint32_t) && s) {
591          current_block++;
592          where = lseek(fd, s, SEEK_CUR);     /* seek after this block */
593       } else {
594          Dmsg4(dbglevel, "read EOF %i:%i nb=%i s=%i\n",
595                current_file, current_block, nb,s);
596          errno = EIO;
597          ret = -1;
598          if (current_file < last_file) {
599             current_block = 0;
600             current_file++;
601             seek_file();
602          }
603          atEOF = true;          /* stop the loop */
604       }
605    }
606
607    find_maxfile();              /* refresh stats */
608
609    return ret;
610 }
611
612 int faketape::bsr(int count)
613 {
614    Dmsg2(dbglevel, "bsr current_block=%i count=%i\n", 
615          current_block, count);
616
617    ASSERT(online);
618    ASSERT(current_file >= 0);
619    ASSERT(count == 1);
620    ASSERT(fd >= 0);
621
622    check_inplace();
623    check_eof();
624
625    if (!count) {
626       return 0;
627    }
628
629    int ret=0;
630    int last_f=0;
631    int last_b=0;
632
633    off_t last=-1, last2=-1;
634    off_t orig = lseek(fd, 0, SEEK_CUR);
635    int orig_f = current_file;
636    int orig_b = current_block;
637
638    /* begin of tape, do nothing */
639    if (atBOT) {
640       errno = EIO;
641       return -1;
642    }
643
644    if (atEOF) {
645       if (!current_block) {
646          if (current_file > 0) {
647             current_file--;
648          }
649          current_block=-1;
650          errno = EIO;
651          return -1;
652
653       } else {
654          atEOF=false;
655       }
656    }
657
658    current_block=0;
659    seek_file();
660
661    do {
662       if (!atEOF) {
663          last2 = last;
664          last = lseek(fd, 0, SEEK_CUR);
665          last_f = current_file;
666          last_b = current_block;
667          Dmsg5(dbglevel, "EOF=%i last=%lli orig=%lli %i:%i\n", 
668                atEOF, last, orig, current_file, current_block);
669       }
670       ret = fsr(1);
671    } while ((lseek(fd, 0, SEEK_CUR) < orig) && (ret == 0));
672
673    if (last2 > 0 && atEOF) {    /* we take the previous position */
674       lseek(fd, last2, SEEK_SET);
675       current_file = last_f;
676       current_block = last_b - 1;
677       Dmsg3(dbglevel, "set offset2=%lli %i:%i\n", 
678             last, current_file, current_block);
679
680    } else if (last > 0) {
681       lseek(fd, last, SEEK_SET);
682       current_file = last_f;
683       current_block = last_b;
684       Dmsg3(dbglevel, "set offset=%lli %i:%i\n", 
685             last, current_file, current_block);
686    } else {
687       lseek(fd, orig, SEEK_SET);
688       current_file = orig_f;
689       current_block = orig_b;
690       return -1;
691    }
692
693    Dmsg2(dbglevel, "bsr %i:%i\n", current_file, current_block);
694    errno=0;
695    atEOT = atEOF = atEOD = false;
696    atBOT = (current_block == 0 && current_file == 0);
697
698    current_block = -1;
699
700    return 0;
701 }
702
703 int faketape::bsf(int count)
704 {
705    ASSERT(online);
706    ASSERT(current_file >= 0);
707    Dmsg3(dbglevel, "bsf %i:%i count=%i\n", current_file, current_block, count);
708    int ret = 0;
709
710    check_inplace();
711    check_eof();
712
713    atBOT = atEOF = atEOT = atEOD = false;
714
715    if (current_file - count < 0) {
716       current_file = 0;
717       current_block = 0;
718       atBOT = true;
719       errno = EIO;
720       ret = -1;
721    } else {
722       current_file = current_file - count + 1;
723       current_block = -1;
724       seek_file();
725       current_file--;
726       /* go just before last EOF */
727       lseek(fd, lseek(fd, 0, SEEK_CUR) - sizeof(uint32_t), SEEK_SET);
728    }
729    return ret;
730 }
731
732 /* 
733  * Put faketape in offline mode
734  */
735 int faketape::offline()
736 {
737    close();
738    
739    atEOF = false;               /* End of file */
740    atEOT = false;               /* End of tape */
741    atEOD = false;               /* End of data */
742    atBOT = false;               /* Begin of tape */
743    online = false;
744
745    file_block = 0;
746    current_file = -1;
747    current_block = -1;
748    last_file = -1;
749    return 0;
750 }
751
752 int faketape::close()
753 {
754    check_eof();
755    ::close(fd);
756    fd = -1;
757    return 0;
758 }
759 /*
760  **rb
761  **status
762  * EOF Bacula status: file=2 block=0
763  * Device status: EOF ONLINE IM_REP_EN file=2 block=0
764  **rb
765  **status
766  * EOD EOF Bacula status: file=2 block=0
767  * Device status: EOD ONLINE IM_REP_EN file=2 block=-1
768  *
769  */
770
771 int faketape::read(void *buffer, unsigned int count)
772 {
773    ASSERT(online);
774    ASSERT(current_file >= 0);
775    unsigned int nb;
776    uint32_t s;
777    
778    Dmsg2(dbglevel, "read %i:%i\n", current_file, current_block);
779
780    if (atEOT || atEOD) {
781       errno = EIO;
782       return -1;
783    }
784
785    if (atEOF) {
786       if (current_file >= last_file) {
787          atEOD = true;
788          atEOF = false;
789          current_block=-1;
790          return 0;
791       }
792       atEOF=false;
793    }
794
795    check_inplace();
796    check_eof();
797
798    atEOD = atBOT = false;
799
800    /* reading size of data */
801    nb = ::read(fd, &s, sizeof(uint32_t));
802    if (nb <= 0) {
803       atEOF = true;             /* TODO: check this */
804       return 0;
805    }
806
807    if (s > count) {             /* not enough buffer to read block */
808       Dmsg2(dbglevel, "Need more buffer to read next block %i > %i\n",s,count);
809       lseek(fd, s, SEEK_CUR);
810       errno = ENOMEM;
811       return -1;
812    }
813
814    if (!s) {                    /* EOF */
815       atEOF = true;
816       if (current_file < last_file) { /* move to next file if possible */
817          current_file++;
818          current_block = 0;
819          inplace=false;
820       }
821       return 0;
822    }
823
824    /* reading data itself */
825    nb = ::read(fd, buffer, s);
826    if (s != nb) {               /* read error */
827       errno=EIO;
828       atEOT = true;
829       current_block = -1;
830       Dmsg0(dbglevel, "EOT during reading\n");
831       return -1;
832    }                    /* read ok */
833
834    if (current_block >= 0) {
835       current_block++;
836    }
837
838    return nb;
839 }
840
841 int faketape::open(const char *pathname, int uflags)
842 {
843    Dmsg2(dbglevel, "faketape::open(%s, %i)\n", pathname, uflags);
844
845    online = true;               /* assume that drive contains a tape */
846
847    struct stat statp;   
848    if (stat(pathname, &statp) != 0) {
849       Dmsg1(dbglevel, "Can't stat on %s\n", pathname);
850       if (uflags & O_NONBLOCK) {
851          online = false;
852          fd = ::open("/dev/null", O_CREAT | O_RDWR | O_LARGEFILE, 0600);
853       }
854    } else {
855       fd = ::open(pathname, O_CREAT | O_RDWR | O_LARGEFILE, 0600);
856    }
857
858    if (fd < 0) {
859       errno = ENOMEDIUM;
860       return -1;
861    }
862
863    /* open volume descriptor and get this->fd */
864    find_maxfile();
865
866    file_block = 0;
867    current_block = 0;
868    current_file = 0;
869    needEOF = false;
870    inplace = true;
871    atBOT = true;
872    atEOT = atEOD = false;
873
874    return fd;
875 }
876
877 /*
878  * read volume to get the last file number
879  */
880 int faketape::find_maxfile()
881 {
882    struct stat statp;
883    if (fstat(fd, &statp) != 0) {
884       return 0;
885    }
886    last_file = statp.st_size>>FILE_OFFSET;
887       
888    Dmsg1(dbglevel+1, "last_file=%i\n", last_file);
889
890    return last_file;
891 }
892
893 void faketape::update_pos()
894 {
895    ASSERT(online);
896    struct stat statp;
897    if (fstat(fd, &statp) == 0) {
898       file_block = statp.st_blocks;
899    } 
900
901    Dmsg1(dbglevel+1, "update_pos=%i\n", file_block);
902
903    if (file_block > max_block) {
904       atEOT = true;
905    } else {
906       atEOT = false;
907    }
908 }
909
910 int faketape::seek_file()
911 {
912    ASSERT(online);
913    ASSERT(current_file >= 0);
914    Dmsg2(dbglevel, "seek_file %i:%i\n", current_file, current_block);
915    inplace = true;
916
917    off_t pos = ((off_t)current_file)<<FILE_OFFSET;
918    if(lseek(fd, pos, SEEK_SET) == -1) {
919       return -1;
920    }
921
922    last_file = MAX(last_file, current_file);
923    if (current_block > 0) {
924       fsr(current_block);
925    }
926
927    return 0;
928 }
929
930 void faketape::dump()
931 {
932    Dmsg0(dbglevel+1, "===================\n");
933    Dmsg2(dbglevel, "file:block = %i:%i\n", current_file, current_block);
934    Dmsg1(dbglevel+1, "last_file=%i\n", last_file);
935    Dmsg1(dbglevel+1, "file_block=%i\n", file_block);  
936    Dmsg4(dbglevel+1, "EOF=%i EOT=%i EOD=%i BOT=%i\n", 
937          atEOF, atEOT, atEOD, atBOT);  
938 }
939
940 #endif /* USE_FAKETAPE */