source: TI05-delivery/trunk/src/bbftp-server-3.2.0/bbftpd/storeafile.c @ 1448

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI05-delivery/trunk/src/bbftp-server-3.2.0/bbftpd/storeafile.c@1448
Revision 1448, 23.2 KB checked in by spascoe, 14 years ago (diff)

Implemented two hooks in the bbftpd source that allow
python code to be executed before and afer a fork(). This is needed
to reset python logging and will probably be usefull for other things
in the future.

Line 
1/*
2 * bbftpd/storeafile.c
3 * Copyright (C) 1999, 2000, 2001, 2002 IN2P3, CNRS
4 * bbftp@in2p3.fr
5 * http://doc.in2p3.fr/bbftp
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
20 */ 
21
22/****************************************************************************
23
24 
25 RETURN:
26         0  Keep the connection open (does not mean that the file has been
27           successfully transfered)
28        -1 Tell the calling program to close the connection
29
30 Child Exit code:
31         255    MES_BAD_NO_RETRY has to be sent to the client.
32        = 0    Success
33        > 0    MES_BAD has to be sent to the client
34       
35        In fact only the first child ending in error will be taken in account.
36        The absolute value of the exit code will be an errno code and the
37        strerror(errno) will be sent as an explanation message.
38 
39 storeafile.c v 1.6.0  2000/03/25   - Creation of the routine. This part of
40                                      code was contained in readcontrol.c
41              v 1.6.1  2000/03/28   - Portage to OSF1
42                                    - use data socket in non bloking mode
43              v 1.8.0  2000/04/14   - Change location of headers file for zlib
44              v 1.8.3  2000/04/18   - Implement better return code to client
45              v 1.8.7  2000/05/24   - Modify headers
46              v 1.8.8  2000/05/31   - Check if file to store is not already a directory
47                                    - Take care of zero length file
48              v 1.8.10 2000/08/11   - Portage to Linux
49                                    - Better start child (wait only if HUP was
50                                      not sent)
51              v 1.9.0  2000/08/18   - Use configure to help portage
52              v 1.9.4  2000/10/16   - Close correctly data socket in child
53                                      in order not to keep socket in TIME_WAIT
54                                      on the server
55              v 2.0.0  2000/10/18   - Set state before starting children
56              v 2.0.1  2001/04/23   - Correct indentation
57              v 2.0.2  2001/05/04   - Correct return code treatment
58              v 2.1.0  2001/05/30   - Correct bbftpd_log level
59                                     
60 *****************************************************************************/
61
62#include <bbftpd.h>
63
64#if HAVE_STRING_H
65# include <string.h>
66#endif
67#include <stdlib.h>
68
69#include <errno.h>
70#include <fcntl.h>
71#include <limits.h>
72#include <netinet/in.h>
73#include <signal.h>
74#include <stdio.h>
75#include <bbftpd_private_log.h>
76#include <bbftpd_private_fork.h>
77#include <sys/stat.h>
78#include <sys/time.h>
79#include <sys/types.h>
80#include <unistd.h>
81
82#include <bbftpd.h>
83#include <common.h>
84#include <daemon.h>
85#include <status.h>
86#include <structures.h>
87
88#include <daemon_proto.h>
89
90#ifdef WITH_GZIP
91#include <zlib.h>
92#endif
93
94extern int flagsighup ;
95extern int msgsock ;
96extern char currentfilename[MAXLENFILE];
97extern int childendinerror ;
98extern int pid_child[MAXPORT] ;
99extern int state ;
100extern int unlinkfile ;
101extern  int     recvcontrolto ;
102extern  int     sendcontrolto ;
103extern  int     datato ;
104
105#ifndef WORDS_BIGENDIAN
106#ifndef HAVE_NTOHLL
107my64_t    ntohll(my64_t v) ;
108#endif
109#endif
110
111int storeafile(int code) {
112
113    int        retcode ;
114    int        i ;
115    char    receive_buffer[MAXMESSLEN] ;
116    struct message *msg ;
117    struct mess_store *msg_store ;
118    struct mess_compress *msg_compress ;
119    int        compressionon ;
120    int        savederrno ;
121
122    char    data_buffer[READBUFLEN] ;
123    char    comp_buffer[READBUFLEN] ;
124   
125    char    logmessage[256] ;
126       
127    int     fd ;
128
129    int    nfds ; /* Max number of file descriptor */
130    fd_set    selectmask ; /* Select mask */
131    struct timeval    wait_timer;
132
133    my64_t    toprint64 ;
134   
135#ifdef STANDART_FILE_CALL
136    off_t         nbperchild ;
137    off_t         nbtoget;
138    off_t         nbget ;
139    off_t         startpoint ;
140    off_t        toseek ;
141    struct stat statbuf ;
142#else
143    off64_t     nbperchild ;
144    off64_t     nbtoget;
145    off64_t     nbget ;
146    off64_t        startpoint ;
147    off64_t        toseek ;
148    struct stat64 statbuf ;
149#endif
150    int lentowrite;
151    int lenwrited;
152    int datatoreceive;
153    int dataonone;
154    int    recsock ;
155
156#ifdef WITH_GZIP   
157    uLong    buflen ;
158    uLong    bufcomplen ;
159#endif
160
161    /*
162    ** Initilize the pid array
163    */
164     for ( i=0 ; i< MAXPORT ; i++) {
165        pid_child[i] = 0 ;
166    }
167    childendinerror = 0 ; /* No child so no error */
168    /*
169    ** Read the characteristics of the file
170    */
171    if ( (retcode = readmessage(msgsock,receive_buffer,STORMESSLEN,recvcontrolto)) < 0 ) {
172        /*
173        ** Error ...
174        */
175        return -1 ;
176    }
177    msg_store = (struct mess_store *)receive_buffer ;
178    strcpy(currentfilename,msg_store->filename) ;
179#ifndef WORDS_BIGENDIAN
180    msg_store->nbport = ntohl(msg_store->nbport) ;
181    for (i = 0 ; i< MAXPORT ; i++ ) {
182        msg_store->port[i] = ntohl(msg_store->port[i]) ;
183    }
184    msg_store->filesize = ntohll(msg_store->filesize) ;
185#endif
186    if ( code == MSG_STORE ) {
187        bbftpd_log(BBFTPD_DEBUG,"Storing file %s of size %" LONG_LONG_FORMAT " with %d children",currentfilename,msg_store->filesize,msg_store->nbport) ;
188    } else {
189        bbftpd_log(BBFTPD_DEBUG,"Storing file %s of size %" LONG_LONG_FORMAT " with %d children in compressed mode",currentfilename,msg_store->filesize,msg_store->nbport) ;
190    }
191    /*
192    ** First stat the file in order to know if it is a directory
193    */
194#ifdef STANDART_FILE_CALL
195    if ( stat(currentfilename,&statbuf) < 0 ) {
196#else
197    if ( stat64(currentfilename,&statbuf) < 0 ) {
198#endif
199        /*
200        ** It may be normal to get an error if the file
201        ** does not exist but some error code must lead
202        ** to the interruption of the transfer:
203        **        EACCES        : Search permission denied
204        **        ELOOP        : To many symbolic links on path
205        **        ENAMETOOLONG: Path argument too long
206        **        ENOTDIR        : A component in path is not a directory
207        */
208        savederrno = errno ;
209        if ( savederrno == EACCES ||
210            savederrno == ELOOP ||
211            savederrno == ENAMETOOLONG ||
212            savederrno == ENOTDIR ) {
213            sprintf(logmessage,"Error stating file %s : %s ",currentfilename,strerror(savederrno)) ;
214            bbftpd_log(BBFTPD_ERR,"Error stating file %s : %s ",currentfilename,strerror(savederrno)) ;
215            reply(MSG_BAD_NO_RETRY,logmessage) ;
216            return 0 ;
217        } else if (savederrno == ENOENT) {
218            /*
219            ** That is normal the file does not exist
220            */
221        } else {
222            sprintf(logmessage,"Error stating file %s : %s ",currentfilename,strerror(savederrno)) ;
223            bbftpd_log(BBFTPD_ERR,"Error stating file %s : %s ",currentfilename,strerror(savederrno)) ;
224            reply(MSG_BAD,logmessage) ;
225            return 0 ;
226        }
227    } else {
228        /*
229        ** The file exists so check if it is a directory
230        */
231        if ( (statbuf.st_mode & S_IFDIR) == S_IFDIR) {
232            bbftpd_log(BBFTPD_ERR,"file %s is a directory",currentfilename) ;
233            sprintf(logmessage,"File %s is a directory",currentfilename) ;
234            reply(MSG_BAD_NO_RETRY,logmessage) ;
235            return 0 ;
236        }
237    }               
238    /*
239    ** We create the file
240    */
241#ifdef STANDART_FILE_CALL
242    if ((fd = open(currentfilename,O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)) < 0 ) {
243#else
244    if ((fd = open64(currentfilename,O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)) < 0 ) {
245#endif
246        /*
247        ** Depending on errno we are going to tell the client to
248        ** retry or not
249        */
250        savederrno = errno ;
251        sprintf(logmessage,"Error creation file %s : %s ",currentfilename,strerror(errno)) ;
252        bbftpd_log(BBFTPD_ERR,"Error creation file %s : %s ",currentfilename,strerror(errno)) ;
253        /*
254        ** We tell the client not to retry in the following case (even in waiting
255        ** WAITRETRYTIME the problem will not be solved) :
256        **        EACCES        : Search permission denied
257        **        EDQUOT        : No more quota
258        **        ENOSPC        : No more space
259        **        ELOOP        : To many symbolic links on path
260        **        ENAMETOOLONG: Path argument too long
261        **        ENOTDIR        : A component in path is not a directory
262        */
263        if ( savederrno == EACCES ||
264                savederrno == EDQUOT ||
265                savederrno == ENOSPC ||
266                savederrno == ELOOP ||
267                savederrno == ENAMETOOLONG ||
268                savederrno == ENOTDIR ) {
269            reply(MSG_BAD_NO_RETRY,logmessage) ;
270        } else {
271            reply(MSG_BAD,logmessage) ;
272        }
273        return 0 ;
274    }
275    /*
276    ** If it is a zero length file close it and reply OK
277    */
278    if ( msg_store->filesize == 0 ) {
279        close(fd) ;
280        reply(MSG_OK,"OK") ;
281        return 0 ;
282    }
283    unlinkfile = 1 ;
284    /*
285    ** Lseek to set it to the correct size
286    */
287    toseek = msg_store->filesize-1 ;
288#ifdef STANDART_FILE_CALL
289    if ( lseek(fd,toseek,SEEK_SET) < 0 ) {
290#else
291    if ( lseek64(fd,toseek,SEEK_SET) < 0 ) {
292#endif
293        sprintf(logmessage,"Error seeking file %s : %s ",currentfilename,strerror(errno)) ;
294        close(fd) ;
295        unlink(currentfilename) ;
296        bbftpd_log(BBFTPD_ERR,"Error seeking file %s : %s ",currentfilename,strerror(errno)) ;
297        reply(MSG_BAD,logmessage) ;
298        return 0 ;
299    }
300    /*
301    ** Write one byte
302    */
303    if ( write(fd,"\0",1) != 1) {
304        savederrno = errno ;
305        sprintf(logmessage,"Error writing file %s : %s ",currentfilename,strerror(errno)) ;
306        close(fd) ;
307        unlink(currentfilename) ;
308        bbftpd_log(BBFTPD_ERR,"Error writing file %s : %s ",currentfilename,strerror(errno)) ;
309        /*
310        ** We tell the client not to retry in the following case (even in waiting
311        ** WAITRETRYTIME the problem will not be solved) :
312        **        EDQUOT        : No more quota
313        **        ENOSPC        : No space on device
314        */
315        if ( savederrno == EDQUOT ||
316                savederrno == ENOSPC ) {
317            reply(MSG_BAD_NO_RETRY,logmessage) ;
318        } else {
319            reply(MSG_BAD,logmessage) ;
320        }
321        return 0 ;
322    }
323    /*
324    ** And close the file
325    */
326    if ( close(fd) < 0 ) {
327        savederrno = errno ;
328        unlink(currentfilename) ;
329        bbftpd_log(BBFTPD_ERR,"Error closing file %s : %s ",currentfilename,strerror(savederrno)) ;
330        sprintf(logmessage,"Error closing file %s : %s ",currentfilename,strerror(savederrno)) ;
331        if ( savederrno == ENOSPC ) {
332            reply(MSG_BAD_NO_RETRY,logmessage) ;
333        } else {
334            reply(MSG_BAD,logmessage) ;
335        }
336        return 0 ;
337    }
338    /*
339    ** We calculate the starting point for each child
340    */
341    nbperchild = msg_store->filesize/msg_store->nbport ;
342    for (i = 1 ; i <= msg_store->nbport ; i++) {
343        if ( i == msg_store->nbport) {
344            startpoint = (i-1)*nbperchild;
345            nbtoget = msg_store->filesize-(nbperchild*(msg_store->nbport-1)) ;
346        } else {
347            startpoint = (i-1)*nbperchild;
348            nbtoget = nbperchild ;
349        }
350               
351        /*
352        ** Now create the socket to receive
353        */
354        recsock = 0 ;
355        while (recsock == 0 ) {
356            recsock = createreceivesock(msg_store->port[i-1],i,logmessage) ;
357        }
358        if ( recsock < 0 ) {
359            unlink(currentfilename) ;
360            /*
361            ** We set childendinerror to 1 in order to prevent the father
362            ** to send a BAD message which can desynchronize the client and the
363            ** server (We need only one error message)
364            ** Bug discovered by amlutz on 2000/03/11
365            */
366            if ( childendinerror == 0 ) {
367                childendinerror = 1 ;
368                reply(MSG_BAD,logmessage) ;
369            }
370            clean_child() ;
371            return 0 ;
372        }
373        /*
374        ** Set flagsighup to zero in order to be able in child
375        ** not to wait STARTCHILDTO if signal was sent before
376        ** entering select. (Seen on Linux with one child)
377        */
378        flagsighup = 0 ;
379        /*
380        ** At this stage we are ready to receive packets
381        ** So we are going to fork
382        */
383        if ( (retcode = bbftpd_fork()) == 0 ) {
384            /*
385            ** We are in child
386            */
387            /*
388            ** Pause until father send a SIGHUP in order to prevent
389            ** child to die before father has started all children
390            */
391            if ( flagsighup == 0) {
392                wait_timer.tv_sec  = STARTCHILDTO ;
393                wait_timer.tv_usec = 0 ;
394                nfds = sysconf(_SC_OPEN_MAX) ;
395                select(nfds,0,0,0,&wait_timer) ;
396            }
397            bbftpd_log(BBFTPD_DEBUG,"Child Starting") ;
398            /*
399            ** Close all unnecessary stuff
400            */
401            close(msgsock) ;
402            /*
403            ** Check if file exist
404            */
405#ifdef STANDART_FILE_CALL
406            if ( stat(currentfilename,&statbuf) < 0 ) {
407#else
408            if ( stat64(currentfilename,&statbuf) < 0 ) {
409#endif
410                /*
411                ** If the file does not exist that means that another
412                ** child has detroyed it
413                */ 
414                i = errno ;
415                bbftpd_log(BBFTPD_ERR,"Error stating file %s : %s ",currentfilename,strerror(errno)) ;
416                close(recsock) ;
417                exit(i) ;
418            }
419            /*
420            ** Open and seek to position
421            */
422#ifdef STANDART_FILE_CALL
423            if ((fd = open(currentfilename,O_RDWR,S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)) < 0 ) {
424#else
425            if ((fd = open64(currentfilename,O_RDWR,S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)) < 0 ) {
426#endif
427                i = errno ;
428                unlink(currentfilename) ;
429                bbftpd_log(BBFTPD_ERR,"Error opening file %s : %s ",currentfilename,strerror(errno)) ;
430                /*
431                ** At this point a non recoverable error is
432                **        EDQUOT        : No more quota
433                **        ENOSPC        : No more space
434                */
435                if ( i == EDQUOT ||
436                        i == ENOSPC ) {
437                    close(recsock) ;
438                    exit(255) ;
439                } else {
440                    close(recsock) ;
441                    exit(i) ;
442                }
443            }
444#ifdef STANDART_FILE_CALL
445            if ( lseek(fd,startpoint,SEEK_SET) < 0 ) {
446#else
447            if ( lseek64(fd,startpoint,SEEK_SET) < 0 ) {
448#endif
449                i = errno ;
450                close(fd) ;
451                unlink(currentfilename) ;
452                bbftpd_log(BBFTPD_ERR,"error seeking file : %s",strerror(errno)) ;
453                close(recsock) ;
454                exit(i)  ;
455            }
456            /*
457            ** start the reading loop
458            */
459            nbget = 0 ;
460            while ( nbget < nbtoget) {
461#ifdef WITH_GZIP
462                if ( code == MSG_STORE_C ) {
463                    /*
464                    ** Receive the header first
465                    */
466                    if (readmessage(recsock,receive_buffer,COMPMESSLEN,datato) < 0 ) {
467                        bbftpd_log(BBFTPD_ERR,"Error reading compression header") ;
468                        close(fd) ;
469                        unlink(currentfilename) ;
470                        i = ETIMEDOUT ;
471                        exit(i) ;
472                    }
473                    msg_compress = ( struct mess_compress *) receive_buffer ;
474#ifndef WORDS_BIGENDIAN
475                    msg_compress->datalen = ntohl(msg_compress->datalen) ;
476#endif
477                    if ( msg_compress->code == DATA_COMPRESS) {
478                        compressionon = 1 ;
479                    } else {
480                        compressionon = 0 ;
481                    }
482                    datatoreceive = msg_compress->datalen ;
483                } else {
484                    /*
485                    ** No compression just adjust the length to receive
486                    */
487                    if ( READBUFLEN <= nbtoget-nbget ) {
488                        datatoreceive = READBUFLEN  ;
489                    } else {
490                        datatoreceive = nbtoget-nbget ;
491                    }
492                }
493#else
494                if ( READBUFLEN <= nbtoget-nbget ) {
495                    datatoreceive = READBUFLEN  ;
496                } else {
497                    datatoreceive = nbtoget-nbget ;
498                }
499#endif
500                /*
501                ** Start the data collection
502                */
503                dataonone = 0 ;
504                while ( dataonone < datatoreceive ) {
505                    nfds = sysconf(_SC_OPEN_MAX) ;
506                    FD_ZERO(&selectmask) ;
507                    FD_SET(recsock,&selectmask) ;
508                    wait_timer.tv_sec  = datato ;
509                    wait_timer.tv_usec = 0 ;
510                    if ( (retcode = select(nfds,&selectmask,0,0,&wait_timer) ) == -1 ) {
511                        /*
512                        ** Select error
513                        */
514                        i = errno ;
515                        bbftpd_log(BBFTPD_ERR,"Error select while receiving : %s",strerror(errno)) ;
516                        close(fd) ;
517                        unlink(currentfilename) ;
518                        close(recsock) ;
519                        exit(i) ;
520                    } else if ( retcode == 0 ) {
521                        bbftpd_log(BBFTPD_ERR,"Time out while receiving") ;
522                        close(fd) ;
523                        unlink(currentfilename) ;
524                        i=ETIMEDOUT ;
525                        close(recsock) ;
526                        exit(i) ;
527                    } else {
528                        retcode = recv(recsock,&data_buffer[dataonone],datatoreceive-dataonone,0) ;
529                        if ( retcode < 0 ) {
530                            i = errno ;
531                            bbftpd_log(BBFTPD_ERR,"Error while receiving : %s",strerror(errno)) ;
532                            close(fd) ;
533                            unlink(currentfilename) ;
534                            close(recsock) ;
535                            exit(i) ;
536                        } else if ( retcode == 0 ) {
537                            i = ECONNRESET ;
538                            bbftpd_log(BBFTPD_ERR,"Connexion breaks") ;
539                            close(fd) ;
540                            unlink(currentfilename) ;
541                            close(recsock) ;
542                            exit(i) ;
543                        } else {
544                            dataonone = dataonone + retcode ;
545                        }
546                    }
547                }
548                /*
549                ** We have received all data needed
550                */
551#ifdef WITH_GZIP
552                if ( code == MSG_STORE_C ) {
553                    if ( compressionon == 1 ) {
554                        bufcomplen = READBUFLEN ;
555                        buflen = dataonone ;
556                        retcode = uncompress((Bytef *) comp_buffer, &bufcomplen, (Bytef *) data_buffer, buflen) ;
557                        if ( retcode != 0 ) {
558                            i = EILSEQ ;
559                            bbftpd_log(BBFTPD_ERR,"Error while decompressing %d ",retcode) ;
560                            close(fd) ;
561                            unlink(currentfilename) ;
562                            close(recsock) ;
563                            exit(i) ;
564                        }
565                        memcpy(data_buffer,comp_buffer,READBUFLEN) ;
566                        lentowrite = bufcomplen ;
567                    } else {
568                        lentowrite = dataonone ;
569                    }
570                } else {
571                    lentowrite = dataonone ;
572                }
573#else
574                lentowrite = dataonone ;
575#endif
576                /*
577                ** Write it to the file
578                */
579                lenwrited = 0 ;
580                while ( lenwrited < lentowrite ) {
581                    if ( (retcode = write(fd,&data_buffer[lenwrited],lentowrite-lenwrited)) < 0 ) {
582                        i = errno ;
583                        bbftpd_log(BBFTPD_ERR,"error writing file : %s",strerror(errno)) ;
584                        close(fd) ;
585                        unlink(currentfilename) ;
586                        if ( i == EDQUOT ||
587                                i == ENOSPC ) {
588                            close(recsock) ;
589                            exit(255) ;
590                        } else {
591                            close(recsock) ;
592                            exit(i) ;
593                        }
594                    } 
595                    lenwrited = lenwrited + retcode ;
596                }
597                nbget = nbget+lenwrited ;
598            }
599            /*
600            ** All data have been received so send the ACK message
601            */
602            msg = (struct message *) data_buffer ;
603            msg->code = MSG_ACK ;
604            msg->msglen = 0 ;
605            if ( writemessage(recsock,data_buffer,MINMESSLEN,sendcontrolto) < 0 ) {
606                close(fd) ;
607                unlink(currentfilename) ;
608                bbftpd_log(BBFTPD_ERR,"Error sending ACK ") ;
609                close(recsock) ;
610                exit(ETIMEDOUT) ;
611            }
612            toprint64 = nbget ;
613            bbftpd_log(BBFTPD_DEBUG,"Child received %" LONG_LONG_FORMAT " bytes ; end correct ",toprint64) ;
614            close(recsock) ;
615            exit(0) ;
616        } else {
617            /*
618            ** We are in father
619            */
620            if ( retcode == -1 ) {
621                /*
622                ** Fork failed ...
623                */
624                bbftpd_log(BBFTPD_ERR,"fork failed : %s",strerror(errno)) ;
625                unlink(currentfilename) ;
626                sprintf(logmessage,"fork failed : %s ",strerror(errno)) ;
627                if ( childendinerror == 0 ) {
628                    childendinerror = 1 ;
629                    reply(MSG_BAD,logmessage) ;
630                }
631                clean_child() ;
632                return 0 ;
633            } else {
634                /*
635                ** Set the parameter telling the sig child routine to unlink
636                ** in case of error
637                */
638                bbftpd_log(BBFTPD_DEBUG,"Started child pid %d",retcode) ;
639                pid_child[i-1] = retcode ;
640                close(recsock) ;
641            }
642        }
643    }
644    /*
645    ** Set the state before starting children because if the file was
646    ** small the child has ended before state was setup to correct value
647    */
648    state = S_RECEIVING ;
649    /*
650    ** Start all children
651    */
652    for (i = 0 ; i<MAXPORT ; i++) {
653        if (pid_child[i] != 0) {
654            kill(pid_child[i],SIGHUP) ;
655        }
656    }
657    return 0 ;
658}
Note: See TracBrowser for help on using the repository browser.