750bd626cae3acee7fa1190903ac20afab224ccb
[debian/amanda] / changer-src / scsi-linux.c
1 /*
2  * Amanda, The Advanced Maryland Automatic Network Disk Archiver
3  * Copyright (c) 1991-2000 University of Maryland at College Park
4  * All Rights Reserved.
5  *
6  * Permission to use, copy, modify, distribute, and sell this software and its
7  * documentation for any purpose is hereby granted without fee, provided that
8  * the above copyright notice appear in all copies and that both that
9  * copyright notice and this permission notice appear in supporting
10  * documentation, and that the name of U.M. not be used in advertising or
11  * publicity pertaining to distribution of the software without specific,
12  * written prior permission.  U.M. makes no representations about the
13  * suitability of this software for any purpose.  It is provided "as is"
14  * without express or implied warranty.
15  *
16  * U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
18  * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
20  * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
21  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22  *
23  * Authors: the Amanda Development Team.  Its members are listed in a
24  * file named AUTHORS, in the root directory of this distribution.
25  */
26 /*
27  * $Id: scsi-linux.c,v 1.28 2006/03/09 20:06:10 johnfranks Exp $
28  *
29  * Interface to execute SCSI commands on Linux
30  *
31  * Copyright (c) Thomas Hepper th@ant.han.de
32  */
33
34
35 #include <amanda.h>
36
37 #ifdef HAVE_DMALLOC_H
38 #include <dmalloc.h>
39 #endif
40
41 #ifdef HAVE_LINUX_LIKE_SCSI
42
43 /*
44 #ifdef HAVE_STDIO_H
45 */
46 #include <stdio.h>
47 /*
48 #endif
49 */
50 #ifdef HAVE_SYS_TYPES_H
51 #include <sys/types.h>
52 #endif
53 #ifdef HAVE_SYS_STAT_H
54 #include <sys/stat.h>
55 #endif
56 #ifdef HAVE_FCNTL_H
57 #include <fcntl.h>
58 #endif
59
60 #ifdef HAVE_DIRENT_H
61 #include <dirent.h>
62 #endif
63
64 #include <time.h>
65
66 #ifdef HAVE_SCSI_SCSI_IOCTL_H
67 #include <scsi/scsi_ioctl.h>
68 #endif
69
70 #ifdef HAVE_SCSI_SG_H
71 #include <scsi/sg.h>
72 #define LINUX_SG
73 #endif
74
75 #ifdef HAVE_SYS_MTIO_H
76 #include <sys/mtio.h>
77 #endif
78
79 #include <scsi-defs.h>
80
81
82 void SCSI_OS_Version()
83 {
84 #ifndef lint
85    static char rcsid[] = "$Id: scsi-linux.c,v 1.28 2006/03/09 20:06:10 johnfranks Exp $";
86    DebugPrint(DEBUG_ERROR, SECTION_INFO, "scsi-os-layer: %s\n",rcsid);
87 #endif
88 }
89
90 int SCSI_CloseDevice(int DeviceFD)
91 {
92   extern OpenFiles_T *pDev;
93   int ret = 0;
94   
95   if (pDev[DeviceFD].devopen == 1)
96     {
97       pDev[DeviceFD].devopen = 0;
98       ret = close(pDev[DeviceFD].fd);
99     }
100
101   return(ret);
102 }
103
104 /* Open a device to talk to an scsi device, either per ioctl, or
105  * direct writing....
106  * Return:
107  * 0 -> error
108  * 1 -> OK
109  *
110  * TODO:
111  * Define some readable defs for the falgs which can be set (like in the AIX dreiver)
112  *
113  */
114 #ifdef LINUX_SG
115 int SCSI_OpenDevice(int ip)
116 {
117   extern OpenFiles_T *pDev;
118   int DeviceFD;
119   int i;
120   int timeout;
121   struct stat pstat;
122   char *buffer = NULL ;           /* Will contain the device name after checking */
123   int openmode = O_RDONLY;
124
125   DebugPrint(DEBUG_INFO, SECTION_SCSI,"##### START SCSI_OpenDevice\n");
126   if (pDev[ip].inqdone == 0)
127     {
128       pDev[ip].inqdone = 1;
129       if (strncmp("/dev/sg", pDev[ip].dev, 7) != 0) /* Check if no sg device for an link .... */
130         {
131           DebugPrint(DEBUG_INFO, SECTION_SCSI,"SCSI_OpenDevice : checking if %s is a sg device\n", pDev[ip].dev);
132           if (lstat(pDev[ip].dev, &pstat) != -1)
133             {
134               if (S_ISLNK(pstat.st_mode) == 1)
135                 {
136                   DebugPrint(DEBUG_INFO, SECTION_SCSI,"SCSI_OpenDevice : is a link, checking destination\n");
137                   if ((buffer = (char *)malloc(512)) == NULL)
138                     {
139                       DebugPrint(DEBUG_ERROR, SECTION_SCSI,"SCSI_OpenDevice : malloc failed\n");
140                       return(0);
141                     }
142                   memset(buffer, 0, 512);
143                   if (( i = readlink(pDev[ip].dev, buffer, 512)) == -1)
144                     {
145                       if (errno == ENAMETOOLONG )
146                         {
147                         } else {
148                           pDev[ip].SCSI = 0;
149                         }
150                     }
151                   if ( i >= 7)
152                     {
153                       if (strncmp("/dev/sg", buffer, 7) == 0)
154                         {
155                           DebugPrint(DEBUG_INFO, SECTION_SCSI,"SCSI_OpenDevice : link points to %s\n", buffer) ;
156                           pDev[ip].flags = 1;
157                         }
158                     }
159                 } else {/* S_ISLNK(pstat.st_mode) == 1 */
160                   DebugPrint(DEBUG_INFO, SECTION_SCSI,"No link %s\n", pDev[ip].dev) ;
161                   buffer = stralloc(pDev[ip].dev);
162                 }
163             } else {/* lstat(DeviceName, &pstat) != -1 */ 
164               DebugPrint(DEBUG_ERROR, SECTION_SCSI,"can't stat device %s\n", pDev[ip].dev);
165               return(0);
166             }
167         } else {
168           buffer = stralloc(pDev[ip].dev);
169           pDev[ip].flags = 1;
170         }
171       
172       if (pDev[ip].flags == 1)
173         {
174           openmode = O_RDWR;
175         }
176       
177       DebugPrint(DEBUG_INFO, SECTION_SCSI,"Try to open %s\n", buffer);
178       if ((DeviceFD = open(buffer, openmode)) >= 0)
179         {
180           pDev[ip].avail = 1;
181           pDev[ip].devopen = 1;
182           pDev[ip].fd = DeviceFD;
183         } else {
184           DebugPrint(DEBUG_INFO, SECTION_SCSI,"##### STOP SCSI_OpenDevice open failed\n");
185           amfree(buffer);
186           return(0);
187         }
188       
189       DebugPrint(DEBUG_INFO, SECTION_SCSI,"done\n");
190       if ( pDev[ip].flags == 1)
191         {
192           pDev[ip].SCSI = 1;
193         }
194       
195       pDev[ip].dev = buffer;
196       if (pDev[ip].SCSI == 1)
197         {
198           DebugPrint(DEBUG_INFO, SECTION_SCSI,"SCSI_OpenDevice : use SG interface\n");
199           if ((timeout = ioctl(pDev[ip].fd, SG_GET_TIMEOUT)) > 0) 
200             {
201               DebugPrint(DEBUG_INFO, SECTION_SCSI,"SCSI_OpenDevice : current timeout %d\n", timeout);
202               timeout = 60000;
203               if (ioctl(pDev[ip].fd, SG_SET_TIMEOUT, &timeout) == 0)
204                 {
205                   DebugPrint(DEBUG_INFO, SECTION_SCSI,"SCSI_OpenDevice : timeout set to %d\n", timeout);
206                 }
207             }
208           pDev[ip].inquiry = (SCSIInquiry_T *)malloc(INQUIRY_SIZE);
209           if (SCSI_Inquiry(ip, pDev[ip].inquiry, INQUIRY_SIZE) == 0)
210             {
211               if (pDev[ip].inquiry->type == TYPE_TAPE || pDev[ip].inquiry->type == TYPE_CHANGER)
212                 {
213                   for (i=0;i < 16;i++)
214                     pDev[ip].ident[i] = pDev[ip].inquiry->prod_ident[i];
215                   for (i=15; i >= 0 && !isalnum(pDev[ip].ident[i]); i--)
216                     {
217                       pDev[ip].ident[i] = '\0';
218                     }
219                   pDev[ip].SCSI = 1;
220
221                   if (pDev[ip].inquiry->type == TYPE_TAPE)
222                   {
223                           pDev[ip].type = stralloc("tape");
224                   }
225
226                   if (pDev[ip].inquiry->type == TYPE_CHANGER)
227                   {
228                           pDev[ip].type = stralloc("changer");
229                   }
230
231                   PrintInquiry(pDev[ip].inquiry);
232                   DebugPrint(DEBUG_INFO, SECTION_SCSI,"##### STOP SCSI_OpenDevice (1)\n");
233                   return(1);
234                 } else {
235                   close(DeviceFD);
236                   amfree(pDev[ip].inquiry);
237                   DebugPrint(DEBUG_INFO, SECTION_SCSI,"##### STOP SCSI_OpenDevice (0)\n");
238                   return(0);
239                 }
240             } else {
241               pDev[ip].SCSI = 0;
242               pDev[ip].devopen = 0;
243               close(DeviceFD);
244               amfree(pDev[ip].inquiry);
245               pDev[ip].inquiry = NULL;
246               DebugPrint(DEBUG_INFO, SECTION_SCSI,"##### STOP SCSI_OpenDevice (1)\n");
247               return(1);
248             }
249         } else /* if (pDev[ip].SCSI == 1) */ {  
250           DebugPrint(DEBUG_INFO, SECTION_SCSI,"Device not capable for SCSI commands\n");
251           pDev[ip].SCSI = 0;
252           pDev[ip].devopen = 0;
253           close(DeviceFD);
254           DebugPrint(DEBUG_INFO, SECTION_SCSI,"##### STOP SCSI_OpenDevice (1)\n");
255           return(1);
256         }
257     } else { /* if (pDev[ip].inqdone == 0) */
258       if (pDev[ip].flags == 1)
259         {
260           openmode = O_RDWR;
261         } else {
262           openmode = O_RDONLY;
263         }
264       if ((DeviceFD = open(pDev[ip].dev, openmode)) >= 0)
265         {
266           pDev[ip].devopen = 1;
267           pDev[ip].fd = DeviceFD;
268           if (pDev[ip].flags == 1)
269             {
270               if ((timeout = ioctl(pDev[ip].fd, SG_GET_TIMEOUT)) > 0) 
271                 {
272                   DebugPrint(DEBUG_INFO, SECTION_SCSI,"SCSI_OpenDevice : current timeout %d\n", timeout);
273                   timeout = 60000;
274                   if (ioctl(pDev[ip].fd, SG_SET_TIMEOUT, &timeout) == 0)
275                     {
276                       DebugPrint(DEBUG_INFO, SECTION_SCSI,"SCSI_OpenDevice : timeout set to %d\n", timeout);
277                     }
278                 }
279             }
280           DebugPrint(DEBUG_INFO, SECTION_SCSI,"##### STOP SCSI_OpenDevice (1)\n");
281           return(1);
282         } else {
283           DebugPrint(DEBUG_INFO, SECTION_SCSI,"##### STOP SCSI_OpenDevice open failed\n");
284           return(0);
285         }
286     }
287   DebugPrint(DEBUG_INFO, SECTION_SCSI,"##### STOP SCSI_OpenDevice should not happen !!\n");
288   return(0);
289 }
290
291 #define SCSI_OFF sizeof(struct sg_header)
292 int SCSI_ExecuteCommand(int DeviceFD,
293                         Direction_T Direction,
294                         CDB_T CDB,
295                         int CDB_Length,
296                         void *DataBuffer,
297                         int DataBufferLength,
298                         char *pRequestSense,
299                         int RequestSenseLength)
300 {
301   extern OpenFiles_T *pDev;
302   struct sg_header *psg_header;
303   char *buffer;
304   int osize = 0;
305   int status;
306
307   if (pDev[DeviceFD].avail == 0)
308     {
309       return(-1);
310     }
311
312   if (pDev[DeviceFD].devopen == 0)
313       if (SCSI_OpenDevice(DeviceFD) == 0)
314           return(-1);
315   
316   if (SCSI_OFF + CDB_Length + DataBufferLength > 4096) 
317     {
318       SCSI_CloseDevice(DeviceFD);
319       return(-1);
320     }
321
322   buffer = (char *)malloc(SCSI_OFF + CDB_Length + DataBufferLength);
323   if (buffer == NULL)
324     {
325       dbprintf(("SCSI_ExecuteCommand memory allocation failure.\n"));
326       SCSI_CloseDevice(DeviceFD);
327       return(-1);
328     }
329   memset(buffer, 0, SCSI_OFF + CDB_Length + DataBufferLength);
330   memcpy(buffer + SCSI_OFF, CDB, CDB_Length);
331   
332   psg_header = (struct sg_header *)buffer;
333   if (CDB_Length >= 12)
334     {
335       psg_header->twelve_byte = 1;
336     } else {
337       psg_header->twelve_byte = 0;
338     }
339   psg_header->result = 0;
340   psg_header->reply_len = SCSI_OFF + DataBufferLength;
341   
342   switch (Direction)
343     {
344     case Input:
345       osize = 0;
346       break;
347     case Output:
348       osize = DataBufferLength;
349       break;
350     }
351   
352   DecodeSCSI(CDB, "SCSI_ExecuteCommand : ");
353   
354   status = write(pDev[DeviceFD].fd, buffer, SCSI_OFF + CDB_Length + osize);
355   if ( status < 0 || status != SCSI_OFF + CDB_Length + osize ||
356        psg_header->result ) 
357     {
358       dbprintf(("SCSI_ExecuteCommand error send \n"));
359       SCSI_CloseDevice(DeviceFD);
360       amfree(buffer);
361       return(SCSI_ERROR);
362     }
363   
364   memset(buffer, 0, SCSI_OFF + DataBufferLength);
365   status = read(pDev[DeviceFD].fd, buffer, SCSI_OFF + DataBufferLength);
366   memset(pRequestSense, 0, RequestSenseLength);
367   memcpy(pRequestSense, psg_header->sense_buffer, 16);
368   
369   if ( status < 0 || status != SCSI_OFF + DataBufferLength || 
370        psg_header->result ) 
371     { 
372       dbprintf(("SCSI_ExecuteCommand error read \n"));
373       dbprintf(("Status %d (%d) %2X\n", status, SCSI_OFF + DataBufferLength,psg_header->result ));
374       SCSI_CloseDevice(DeviceFD);
375       amfree(buffer);
376       return(SCSI_ERROR);
377     }
378
379   if (DataBufferLength)
380     {
381        memcpy(DataBuffer, buffer + SCSI_OFF, DataBufferLength);
382     }
383
384   SCSI_CloseDevice(DeviceFD);
385   amfree(buffer);
386   return(SCSI_OK);
387 }
388
389 #else
390
391 static inline int min(int x, int y)
392 {
393   return (x < y ? x : y);
394 }
395
396
397 static inline int max(int x, int y)
398 {
399   return (x > y ? x : y);
400 }
401
402 int SCSI_OpenDevice(int ip)
403 {
404   extern OpenFiles_T *pDev;
405   int DeviceFD;
406   int i;
407
408   if (pDev[ip].inqdone == 0)
409     {
410       pDev[ip].inqdone = 1;
411       if ((DeviceFD = open(pDev[ip].dev, O_RDWR)) >= 0)
412         {
413           pDev[ip].avail = 1;
414           pDev[ip].fd = DeviceFD;
415           pDev[ip].SCSI = 0;
416           pDev[ip].inquiry = (SCSIInquiry_T *)malloc(INQUIRY_SIZE);
417           dbprintf(("SCSI_OpenDevice : use ioctl interface\n"));
418           if (SCSI_Inquiry(ip, pDev[ip].inquiry, INQUIRY_SIZE) == 0)
419             {
420               if (pDev[ip].inquiry->type == TYPE_TAPE || pDev[ip].inquiry->type == TYPE_CHANGER)
421                 {
422                   for (i=0;i < 16 && pDev[ip].inquiry->prod_ident[i] != ' ';i++)
423                     pDev[ip].ident[i] = pDev[ip].inquiry->prod_ident[i];
424                   pDev[ip].ident[i] = '\0';
425                   pDev[ip].SCSI = 1;
426                   PrintInquiry(pDev[ip].inquiry);
427                   return(1);
428                 } else {
429                   amfree(pDev[ip].inquiry);
430                   close(DeviceFD);
431                   return(0);
432                 }
433             } else {
434               close(DeviceFD);
435               amfree(pDev[ip].inquiry);
436               pDev[ip].inquiry = NULL;
437               return(1);
438             }
439         }
440       return(1); 
441     } else {
442       if ((DeviceFD = open(pDev[ip].dev, O_RDWR)) >= 0)
443         {
444           pDev[ip].fd = DeviceFD;
445           pDev[ip].devopen = 1;
446           return(1);
447         } else {
448           pDev[ip].devopen = 0;
449           return(0);
450         }
451     }
452 }
453
454 int SCSI_ExecuteCommand(int DeviceFD,
455                         Direction_T Direction,
456                         CDB_T CDB,
457                         int CDB_Length,
458                         void *DataBuffer,
459                         int DataBufferLength,
460                         char *pRequestSense,
461                         int RequestSenseLength)
462 {
463   extern OpenFiles_T *pDev;
464   unsigned char *Command;
465   int Zero = 0, Result;
466  
467   if (pDev[DeviceFD].avail == 0)
468     {
469       return(SCSI_ERROR);
470     }
471
472   if (pDev[DeviceFD].devopen == 0)
473     {
474       if (SCSI_OpenDevice(DeviceFD) == 0)
475           return(-1);
476     }
477
478   memset(pRequestSense, 0, RequestSenseLength);
479   switch (Direction)
480     {
481     case Input:
482       Command = (unsigned char *)
483         malloc(8 + max(DataBufferLength, RequestSenseLength));
484       memcpy(&Command[0], &Zero, 4);
485       memcpy(&Command[4], &DataBufferLength, 4);
486       memcpy(&Command[8], CDB, CDB_Length);
487       break;
488     case Output:
489       Command = (unsigned char *)
490         malloc(8 + max(CDB_Length + DataBufferLength, RequestSenseLength));
491       memcpy(&Command[0], &DataBufferLength, 4);
492       memcpy(&Command[4], &Zero, 4);
493       memcpy(&Command[8], CDB, CDB_Length);
494       memcpy(&Command[8 + CDB_Length], DataBuffer, DataBufferLength);
495       break;
496     }
497   
498   DecodeSCSI(CDB, "SCSI_ExecuteCommand : ");
499   
500   Result = ioctl(pDev[DeviceFD].fd, SCSI_IOCTL_SEND_COMMAND, Command);
501   if (Result != 0)
502     memcpy(pRequestSense, &Command[8], RequestSenseLength);
503   else if (Direction == Input)
504     memcpy(DataBuffer, &Command[8], DataBufferLength);
505   amfree(Command);
506   SCSI_CloseDevice(DeviceFD);
507
508   switch(Result)
509     {
510       case 0:
511         return(SCSI_OK);
512         break;
513     default:
514       return(SCSI_SENSE);
515       break;
516     }
517 }
518 #endif
519
520 /*
521  * Send the command to the device with the
522  * ioctl interface
523  */
524 int Tape_Ioctl( int DeviceFD, int command)
525 {
526   extern OpenFiles_T *pDev;
527   struct mtop mtop;
528   int ret = 0;
529
530   if (pDev[DeviceFD].devopen == 0)
531     {
532       if (SCSI_OpenDevice(DeviceFD) == 0)
533           return(-1);
534     }
535
536   switch (command)
537     {
538     case IOCTL_EJECT:
539       mtop.mt_op = MTOFFL;
540       mtop.mt_count = 1;
541       break;
542      default:
543       break;
544     }
545
546   if (ioctl(pDev[DeviceFD].fd , MTIOCTOP, &mtop) != 0)
547     {
548       dbprintf(("Tape_Ioctl error ioctl %d\n",errno));
549       SCSI_CloseDevice(DeviceFD);
550       return(-1);
551     }
552
553   SCSI_CloseDevice(DeviceFD);
554   return(ret);  
555 }
556
557 int Tape_Status( int DeviceFD)
558 {
559   extern OpenFiles_T *pDev;
560   struct mtget mtget;
561   int ret = 0;
562
563   if (pDev[DeviceFD].devopen == 0)
564     {
565       if (SCSI_OpenDevice(DeviceFD) == 0)
566           return(-1);
567     }
568
569   if (ioctl(pDev[DeviceFD].fd , MTIOCGET, &mtget) != 0)
570   {
571      DebugPrint(DEBUG_ERROR, SECTION_TAPE,"Tape_Status error ioctl %d\n",errno);
572      SCSI_CloseDevice(DeviceFD);
573      return(-1);
574   }
575
576   DebugPrint(DEBUG_INFO, SECTION_TAPE,"ioctl -> mtget.mt_gstat %lX\n",mtget.mt_gstat);
577   if (GMT_ONLINE(mtget.mt_gstat))
578     {
579       ret = TAPE_ONLINE;
580     }
581   
582   if (GMT_BOT(mtget.mt_gstat))
583     {
584       ret = ret | TAPE_BOT;
585     }
586   
587   if (GMT_EOT(mtget.mt_gstat))
588     {
589       ret = ret | TAPE_EOT;
590     }
591   
592   if (GMT_WR_PROT(mtget.mt_gstat))
593     {
594       ret = ret | TAPE_WR_PROT;
595     }
596   
597   if (GMT_DR_OPEN(mtget.mt_gstat))
598     {
599       ret = ret | TAPE_NOT_LOADED;
600     }
601   
602   SCSI_CloseDevice(DeviceFD);
603   return(ret); 
604 }
605
606 /*
607  * This functions scan all /dev/sg* devices
608  * It opens the device an print the result of the inquiry 
609  *
610  */
611 int ScanBus(int print)
612 {
613   DIR *dir;
614   struct dirent *dirent;
615   extern OpenFiles_T *pDev;
616   extern int errno;
617   int count = 0;
618
619   if ((dir = opendir("/dev/")) == NULL)
620     {
621       dbprintf(("/dev/ error: %s", strerror(errno)));
622       return 0;
623     }
624
625   while ((dirent = readdir(dir)) != NULL)
626     {
627       if (strstr(dirent->d_name, "sg") != NULL)
628       {
629         pDev[count].dev = malloc(10);
630         pDev[count].inqdone = 0;
631         sprintf(pDev[count].dev,"/dev/%s", dirent->d_name);
632         if (OpenDevice(count,pDev[count].dev, "Scan", NULL ))
633           {
634             SCSI_CloseDevice(count);
635             pDev[count].inqdone = 0;
636             
637             if (print)
638               {
639                 printf("name /dev/%s ", dirent->d_name);
640                 
641                 switch (pDev[count].inquiry->type)
642                   {
643                   case TYPE_DISK:
644                     printf("Disk");
645                     break;
646                   case TYPE_TAPE:
647                     printf("Tape");
648                     break;
649                   case TYPE_PRINTER:
650                     printf("Printer");
651                     break;
652                   case TYPE_PROCESSOR:
653                     printf("Processor");
654                     break;
655                   case TYPE_WORM:
656                     printf("Worm");
657                     break;
658                   case TYPE_CDROM:
659                     printf("Cdrom");
660                     break;
661                   case TYPE_SCANNER:
662                     printf("Scanner");
663                     break;
664                   case TYPE_OPTICAL:
665                     printf("Optical");
666                     break;
667                   case TYPE_CHANGER:
668                     printf("Changer");
669                     break;
670                   case TYPE_COMM:
671                     printf("Comm");
672                     break;
673                   default:
674                     printf("unknown %d",pDev[count].inquiry->type);
675                     break;
676                   }
677                 printf("\n");
678               }
679             count++;
680             printf("Count %d\n",count);
681           } else {
682             amfree(pDev[count].dev);
683             pDev[count].dev=NULL;
684           }
685       }
686     }
687   return 0;
688 }
689 #endif
690 /*
691  * Local variables:
692  * indent-tabs-mode: nil
693  * c-file-style: gnu
694  * End:
695  */