source: branches/pex/libtransmission/torrent.c @ 1540

Last change on this file since 1540 was 1540, checked in by joshe, 15 years ago

Implement Azureus peer protocol, including PEX message.
Implement extended messages, including uTorrent PEX.

  • Property svn:keywords set to Date Rev Author Id
File size: 20.5 KB
Line 
1/******************************************************************************
2 * $Id: torrent.c 1540 2007-03-08 04:06:58Z joshe $
3 *
4 * Copyright (c) 2005-2007 Transmission authors and contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
24
25#include "transmission.h"
26#include "shared.h"
27
28/***********************************************************************
29 * Local prototypes
30 **********************************************************************/
31static tr_torrent_t * torrentRealInit( tr_handle_t *, tr_torrent_t * tor,
32                                       int flags, int * error );
33static void torrentReallyStop( tr_torrent_t * );
34static void downloadLoop( void * );
35
36void tr_setUseCustomUpload( tr_torrent_t * tor, int limit )
37{
38    tor->customUploadLimit = limit;
39}
40
41void tr_setUseCustomDownload( tr_torrent_t * tor, int limit )
42{
43    tor->customDownloadLimit = limit;
44}
45
46void tr_setUploadLimit( tr_torrent_t * tor, int limit )
47{
48    tr_rcSetLimit( tor->upload, limit );
49}
50
51void tr_setDownloadLimit( tr_torrent_t * tor, int limit )
52{
53    tr_rcSetLimit( tor->download, limit );
54}
55
56tr_torrent_t * tr_torrentInit( tr_handle_t * h, const char * path,
57                               int flags, int * error )
58{
59    tr_torrent_t  * tor = calloc( sizeof( tr_torrent_t ), 1 );
60    int             saveCopy = ( TR_FLAG_SAVE & flags );
61
62    /* Parse torrent file */
63    if( tr_metainfoParse( &tor->info, path, NULL, saveCopy ) )
64    {
65        *error = TR_EINVALID;
66        free( tor );
67        return NULL;
68    }
69
70    return torrentRealInit( h, tor, flags, error );
71}
72
73tr_torrent_t * tr_torrentInitSaved( tr_handle_t * h, const char * hashStr,
74                                    int flags, int * error )
75{
76    tr_torrent_t  * tor = calloc( sizeof( tr_torrent_t ), 1 );
77
78    /* Parse torrent file */
79    if( tr_metainfoParse( &tor->info, NULL, hashStr, 0 ) )
80    {
81        *error = TR_EINVALID;
82        free( tor );
83        return NULL;
84    }
85
86    return torrentRealInit( h, tor, ( TR_FLAG_SAVE | flags ), error );
87}
88
89/***********************************************************************
90 * tr_torrentInit
91 ***********************************************************************
92 * Allocates a tr_torrent_t structure, then relies on tr_metainfoParse
93 * to fill it.
94 **********************************************************************/
95static tr_torrent_t * torrentRealInit( tr_handle_t * h, tr_torrent_t * tor,
96                                       int flags, int * error )
97{
98    tr_torrent_t  * tor_tmp;
99    tr_info_t     * inf;
100    int             i;
101   
102    inf         = &tor->info;
103    inf->flags |= flags;
104
105    tr_sharedLock( h->shared );
106
107    /* Make sure this torrent is not already open */
108    for( tor_tmp = h->torrentList; tor_tmp; tor_tmp = tor_tmp->next )
109    {
110        if( !memcmp( tor->info.hash, tor_tmp->info.hash,
111                     SHA_DIGEST_LENGTH ) )
112        {
113            *error = TR_EDUPLICATE;
114            tr_metainfoFree( &tor->info );
115            free( tor );
116            tr_sharedUnlock( h->shared );
117            return NULL;
118        }
119    }
120
121    tor->handle   = h;
122    tor->status   = TR_STATUS_PAUSE;
123    tor->id       = h->id;
124    tor->key      = h->key;
125    tor->azId     = h->azId;
126    tor->finished = 0;
127
128    /* Escaped info hash for HTTP queries */
129    for( i = 0; i < SHA_DIGEST_LENGTH; i++ )
130    {
131        sprintf( &tor->escapedHashString[3*i], "%%%02x", inf->hash[i] );
132    }
133
134    /* Block size: usually 16 ko, or less if we have to */
135    tor->blockSize  = MIN( inf->pieceSize, 1 << 14 );
136    tor->blockCount = ( inf->totalSize + tor->blockSize - 1 ) /
137                        tor->blockSize;
138    tor->completion = tr_cpInit( tor );
139
140    tr_lockInit( &tor->lock );
141    tr_condInit( &tor->cond );
142
143    tor->upload         = tr_rcInit();
144    tor->download       = tr_rcInit();
145    tor->swarmspeed     = tr_rcInit();
146 
147    /* We have a new torrent */
148    tor->publicPort = tr_sharedGetPublicPort( h->shared );
149    tor->prev       = NULL;
150    tor->next       = h->torrentList;
151    if( tor->next )
152    {
153        tor->next->prev = tor;
154    }
155    h->torrentList = tor;
156    (h->torrentCount)++;
157
158    tr_sharedUnlock( h->shared );
159
160    if( !h->isPortSet )
161    {
162        tr_setBindPort( h, TR_DEFAULT_PORT );
163    }
164
165    return tor;
166}
167
168tr_info_t * tr_torrentInfo( tr_torrent_t * tor )
169{
170    return &tor->info;
171}
172
173/***********************************************************************
174 * tr_torrentScrape     
175 **********************************************************************/
176int tr_torrentScrape( tr_torrent_t * tor, int * s, int * l, int * d )
177{
178    return tr_trackerScrape( tor, s, l, d );
179}
180
181void tr_torrentSetFolder( tr_torrent_t * tor, const char * path )
182{
183    tor->destination = strdup( path );
184    tr_ioLoadResume( tor );
185}
186
187char * tr_torrentGetFolder( tr_torrent_t * tor )
188{
189    return tor->destination;
190}
191
192void tr_torrentStart( tr_torrent_t * tor )
193{
194    char name[32];
195
196    if( tor->status & ( TR_STATUS_STOPPING | TR_STATUS_STOPPED ) )
197    {
198        /* Join the thread first */
199        torrentReallyStop( tor );
200    }
201
202    tr_lockLock( &tor->lock );
203
204    tor->downloadedPrev += tor->downloadedCur;
205    tor->downloadedCur   = 0;
206    tor->uploadedPrev   += tor->uploadedCur;
207    tor->uploadedCur     = 0;
208
209    tor->status  = TR_STATUS_CHECK;
210    tor->error   = TR_OK;
211    tor->tracker = tr_trackerInit( tor );
212
213    tor->date = tr_date();
214    tor->die = 0;
215    snprintf( name, sizeof( name ), "torrent %p", tor );
216
217    tr_lockUnlock( &tor->lock );
218
219    tr_threadCreate( &tor->thread, downloadLoop, tor, name );
220}
221
222static void torrentStop( tr_torrent_t * tor )
223{
224    tr_trackerStopped( tor->tracker );
225    tr_rcReset( tor->download );
226    tr_rcReset( tor->upload );
227    tr_rcReset( tor->swarmspeed );
228    tor->status = TR_STATUS_STOPPING;
229    tor->stopDate = tr_date();
230}
231
232void tr_torrentStop( tr_torrent_t * tor )
233{
234    tr_lockLock( &tor->lock );
235    torrentStop( tor );
236
237    /* Don't return until the files are closed, so the UI can trash
238     * them if requested */
239    tr_condWait( &tor->cond, &tor->lock );
240    tr_lockUnlock( &tor->lock );
241}
242
243/***********************************************************************
244 * torrentReallyStop
245 ***********************************************************************
246 * Joins the download thread and frees/closes everything related to it.
247 **********************************************************************/
248static void torrentReallyStop( tr_torrent_t * tor )
249{
250    int i;
251
252    tor->die = 1;
253    tr_threadJoin( &tor->thread );
254
255    tr_trackerClose( tor->tracker );
256    tor->tracker = NULL;
257
258    tr_lockLock( &tor->lock );
259    for( i = 0; i < tor->peerCount; i++ )
260    {
261        tr_peerDestroy( tor->peers[i] );
262    }
263    tor->peerCount = 0;
264    tr_lockUnlock( &tor->lock );
265}
266
267int tr_getFinished( tr_torrent_t * tor )
268{
269    if( tor->finished )
270    {
271        tor->finished = 0;
272        return 1;
273    }
274    return 0;
275}
276
277void tr_manualUpdate( tr_torrent_t * tor )
278{
279    int peerCount;
280    uint8_t * peerCompact;
281
282    if( !( tor->status & TR_STATUS_ACTIVE ) )
283        return;
284   
285    tr_lockLock( &tor->lock );
286    tr_trackerAnnouncePulse( tor->tracker, &peerCount, &peerCompact, 1 );
287    if( peerCount > 0 )
288    {
289        tr_torrentAddCompact( tor, TR_PEER_FROM_TRACKER,
290                              peerCompact, peerCount );
291        free( peerCompact );
292    }
293    tr_lockUnlock( &tor->lock );
294}
295
296tr_stat_t * tr_torrentStat( tr_torrent_t * tor )
297{
298    tr_stat_t * s;
299    tr_peer_t * peer;
300    tr_info_t * inf = &tor->info;
301    tr_tracker_t * tc;
302    int i;
303
304    tor->statCur = ( tor->statCur + 1 ) % 2;
305    s = &tor->stats[tor->statCur];
306
307    if( ( tor->status & TR_STATUS_STOPPED ) ||
308        ( ( tor->status & TR_STATUS_STOPPING ) &&
309          tr_date() > tor->stopDate + 60000 ) )
310    {
311        torrentReallyStop( tor );
312        tor->status = TR_STATUS_PAUSE;
313    }
314
315    tr_lockLock( &tor->lock );
316
317    s->status = tor->status;
318    s->error  = tor->error;
319    memcpy( s->errorString, tor->errorString,
320            sizeof( s->errorString ) );
321
322    tc = tor->tracker;
323    s->cannotConnect = tr_trackerCannotConnect( tc );
324    s->tracker = ( tc ? tr_trackerGet( tc ) : &inf->trackerList[0].list[0] );
325
326    s->peersTotal       = 0;
327    bzero( s->peersFrom, sizeof( s->peersFrom ) );
328    s->peersUploading   = 0;
329    s->peersDownloading = 0;
330   
331    for( i = 0; i < tor->peerCount; i++ )
332    {
333        peer = tor->peers[i];
334   
335        if( tr_peerIsConnected( peer ) )
336        {
337            (s->peersTotal)++;
338            (s->peersFrom[ tr_peerIsFrom( peer ) ])++;
339            if( tr_peerAmInterested( peer ) && !tr_peerIsChoking( peer ) )
340            {
341                (s->peersUploading)++;
342            }
343            if( !tr_peerAmChoking( peer ) )
344            {
345                (s->peersDownloading)++;
346            }
347        }
348    }
349
350    s->progress = tr_cpCompletionAsFloat( tor->completion );
351    if( tor->status & TR_STATUS_DOWNLOAD )
352    {
353        s->rateDownload = tr_rcRate( tor->download );
354    }
355    else
356    {
357        /* tr_rcRate() doesn't make the difference between 'piece'
358           messages and other messages, which causes a non-zero
359           download rate even tough we are not downloading. So we
360           force it to zero not to confuse the user. */
361        s->rateDownload = 0.0;
362    }
363    s->rateUpload = tr_rcRate( tor->upload );
364   
365    s->seeders  = tr_trackerSeeders( tc );
366    s->leechers = tr_trackerLeechers( tc );
367    s->completedFromTracker = tr_trackerDownloaded( tc );
368
369    s->swarmspeed = tr_rcRate( tor->swarmspeed );
370
371    if( s->rateDownload < 0.1 )
372    {
373        s->eta = -1;
374    }
375    else
376    {
377        s->eta = ( 1.0 - s->progress ) *
378            (float) inf->totalSize / s->rateDownload / 1024.0;
379    }
380
381    s->downloaded = tor->downloadedCur + tor->downloadedPrev;
382    s->uploaded   = tor->uploadedCur   + tor->uploadedPrev;
383   
384    if( s->downloaded == 0 )
385    {
386        s->ratio = s->uploaded == 0 ? TR_RATIO_NA : TR_RATIO_INF;
387    }
388    else
389    {
390        s->ratio = (float)s->uploaded / (float)s->downloaded;
391    }
392   
393    tr_lockUnlock( &tor->lock );
394
395    return s;
396}
397
398tr_peer_stat_t * tr_torrentPeers( tr_torrent_t * tor, int * peerCount )
399{
400    tr_peer_stat_t * peers;
401
402    tr_lockLock( &tor->lock );
403
404    *peerCount = tor->peerCount;
405   
406    peers = (tr_peer_stat_t *) calloc( tor->peerCount, sizeof( tr_peer_stat_t ) );
407    if (peers != NULL)
408    {
409        tr_peer_t * peer;
410        struct in_addr * addr;
411        int i;
412        for( i = 0; i < tor->peerCount; i++ )
413        {
414            peer = tor->peers[i];
415           
416            addr = tr_peerAddress( peer );
417            if( NULL != addr )
418            {
419                tr_netNtop( addr, peers[i].addr,
420                           sizeof( peers[i].addr ) );
421            }
422           
423            peers[i].client = tr_clientForId(tr_peerId(peer));
424           
425            peers[i].isConnected   = tr_peerIsConnected( peer );
426            peers[i].from          = tr_peerIsFrom( peer );
427            peers[i].progress      = tr_peerProgress( peer );
428            peers[i].port          = tr_peerPort( peer );
429           
430            if( ( peers[i].isDownloading = !tr_peerAmChoking( peer ) ) )
431            {
432                peers[i].uploadToRate = tr_peerUploadRate( peer );
433            }
434            if( ( peers[i].isUploading = ( tr_peerAmInterested( peer ) &&
435                                           !tr_peerIsChoking( peer ) ) ) )
436            {
437                peers[i].downloadFromRate = tr_peerDownloadRate( peer );
438            }
439        }
440    }
441   
442    tr_lockUnlock( &tor->lock );
443   
444    return peers;
445}
446
447void tr_torrentPeersFree( tr_peer_stat_t * peers, int peerCount )
448{
449    int i;
450
451    if (peers == NULL)
452        return;
453
454    for (i = 0; i < peerCount; i++)
455        free( peers[i].client );
456
457    free( peers );
458}
459
460void tr_torrentAvailability( tr_torrent_t * tor, int8_t * tab, int size )
461{
462    int i, j, piece;
463    float interval;
464
465    tr_lockLock( &tor->lock );
466    interval = (float)tor->info.pieceCount / (float)size;
467    for( i = 0; i < size; i++ )
468    {
469        piece = i * interval;
470
471        if( tr_cpPieceIsComplete( tor->completion, piece ) )
472        {
473            tab[i] = -1;
474            continue;
475        }
476
477        tab[i] = 0;
478        for( j = 0; j < tor->peerCount; j++ )
479        {
480            if( tr_peerBitfield( tor->peers[j] ) &&
481                tr_bitfieldHas( tr_peerBitfield( tor->peers[j] ), piece ) )
482            {
483                (tab[i])++;
484            }
485        }
486    }
487    tr_lockUnlock( &tor->lock );
488}
489
490float * tr_torrentCompletion( tr_torrent_t * tor )
491{
492    tr_info_t * inf = &tor->info;
493    int         piece, file;
494    float     * ret, prog, weight;
495    uint64_t    piecemax, piecesize;
496    uint64_t    filestart, fileoff, filelen, blockend, blockused;
497
498    tr_lockLock( &tor->lock );
499
500    ret       = calloc( inf->fileCount, sizeof( float ) );
501    file      = 0;
502    piecemax  = inf->pieceSize;
503    filestart = 0;
504    fileoff   = 0;
505    piece     = 0;
506    while( inf->pieceCount > piece )
507    {
508        assert( file < inf->fileCount );
509        assert( filestart + fileoff < inf->totalSize );
510        filelen    = inf->files[file].length;
511        piecesize  = tr_pieceSize( piece );
512        blockend   = MIN( filestart + filelen, piecemax * piece + piecesize );
513        blockused  = blockend - ( filestart + fileoff );
514        weight     = ( filelen ? ( float )blockused / ( float )filelen : 1.0 );
515        prog       = tr_cpPercentBlocksInPiece( tor->completion, piece );
516        ret[file] += prog * weight;
517        fileoff   += blockused;
518        assert( -0.1 < prog   && 1.1 > prog );
519        assert( -0.1 < weight && 1.1 > weight );
520        if( fileoff == filelen )
521        {
522            ret[file] = MIN( 1.0, ret[file] );
523            ret[file] = MAX( 0.0, ret[file] );
524            filestart += fileoff;
525            fileoff    = 0;
526            file++;
527        }
528        if( filestart + fileoff >= piecemax * piece + piecesize )
529        {
530            piece++;
531        }
532    }
533
534    tr_lockUnlock( &tor->lock );
535
536    return ret;
537}
538
539void tr_torrentAmountFinished( tr_torrent_t * tor, float * tab, int size )
540{
541    int i, piece;
542    float interval;
543
544    tr_lockLock( &tor->lock );
545    interval = (float)tor->info.pieceCount / (float)size;
546    for( i = 0; i < size; i++ )
547    {
548        piece = i * interval;
549        tab[i] = tr_cpPercentBlocksInPiece( tor->completion, piece );
550    }
551    tr_lockUnlock( &tor->lock );
552}
553
554void tr_torrentRemoveSaved( tr_torrent_t * tor )
555{
556    tr_metainfoRemoveSaved( tor->info.hashString );
557}
558
559/***********************************************************************
560 * tr_torrentClose
561 ***********************************************************************
562 * Frees memory allocated by tr_torrentInit.
563 **********************************************************************/
564void tr_torrentClose( tr_handle_t * h, tr_torrent_t * tor )
565{
566    tr_info_t * inf = &tor->info;
567
568    if( tor->status & ( TR_STATUS_STOPPING | TR_STATUS_STOPPED ) )
569    {
570        /* Join the thread first */
571        torrentReallyStop( tor );
572    }
573
574    tr_sharedLock( h->shared );
575
576    h->torrentCount--;
577
578    tr_lockClose( &tor->lock );
579    tr_condClose( &tor->cond );
580    tr_cpClose( tor->completion );
581
582    tr_rcClose( tor->upload );
583    tr_rcClose( tor->download );
584    tr_rcClose( tor->swarmspeed );
585
586    if( tor->destination )
587    {
588        free( tor->destination );
589    }
590
591    tr_metainfoFree( inf );
592
593    if( tor->prev )
594    {
595        tor->prev->next = tor->next;
596    }
597    else
598    {
599        h->torrentList = tor->next;
600    }
601    if( tor->next )
602    {
603        tor->next->prev = tor->prev;
604    }
605    free( tor );
606
607    tr_sharedUnlock( h->shared );
608}
609
610void tr_torrentAttachPeer( tr_torrent_t * tor, tr_peer_t * peer )
611{
612    int i;
613    tr_peer_t * otherPeer;
614
615    if( tor->peerCount >= TR_MAX_PEER_COUNT )
616    {
617        tr_peerDestroy(  peer );
618        return;
619    }
620
621    /* Don't accept two connections from the same IP */
622    for( i = 0; i < tor->peerCount; i++ )
623    {
624        otherPeer = tor->peers[i];
625        if( !memcmp( tr_peerAddress( peer ), tr_peerAddress( otherPeer ), 4 ) )
626        {
627            tr_peerDestroy(  peer );
628            return;
629        }
630    }
631
632    tr_peerSetPrivate( peer, tor->info.flags & TR_FLAG_PRIVATE );
633    tr_peerSetTorrent( peer, tor );
634    tor->peers[tor->peerCount++] = peer;
635}
636
637void tr_torrentAddCompact( tr_torrent_t * tor, int from,
638                           uint8_t * buf, int count )
639{
640    struct in_addr addr;
641    in_port_t port;
642    int i;
643    tr_peer_t * peer;
644
645    for( i = 0; i < count; i++ )
646    {
647        memcpy( &addr, buf, 4 ); buf += 4;
648        memcpy( &port, buf, 2 ); buf += 2;
649
650        peer = tr_peerInit( addr, port, -1, from );
651        tr_torrentAttachPeer( tor, peer );
652    }
653}
654
655/***********************************************************************
656 * downloadLoop
657 **********************************************************************/
658static void downloadLoop( void * _tor )
659{
660    tr_torrent_t * tor = _tor;
661    int            i, ret;
662    int            peerCount;
663    uint8_t      * peerCompact;
664    tr_peer_t    * peer;
665
666    tr_lockLock( &tor->lock );
667
668    tr_cpReset( tor->completion );
669    tor->io     = tr_ioInit( tor );
670    tor->status = tr_cpIsSeeding( tor->completion ) ?
671                      TR_STATUS_SEED : TR_STATUS_DOWNLOAD;
672
673    while( !tor->die )
674    {
675        tr_lockUnlock( &tor->lock );
676        tr_wait( 20 );
677        tr_lockLock( &tor->lock );
678
679        /* Are we finished ? */
680        if( ( tor->status & TR_STATUS_DOWNLOAD ) &&
681            tr_cpIsSeeding( tor->completion ) )
682        {
683            /* Done */
684            tor->status = TR_STATUS_SEED;
685                        tor->finished = 1;
686            tr_trackerCompleted( tor->tracker );
687            tr_ioSync( tor->io );
688        }
689
690        /* Try to get new peers or to send a message to the tracker */
691        tr_trackerPulse( tor->tracker, &peerCount, &peerCompact );
692        if( peerCount > 0 )
693        {
694            tr_torrentAddCompact( tor, TR_PEER_FROM_TRACKER,
695                                  peerCompact, peerCount );
696            free( peerCompact );
697        }
698        if( tor->status & TR_STATUS_STOPPED )
699        {
700            break;
701        }
702
703        /* Stopping: make sure all files are closed and stop talking
704           to peers */
705        if( tor->status & TR_STATUS_STOPPING )
706        {
707            if( tor->io )
708            {
709                tr_ioClose( tor->io ); tor->io = NULL;
710                tr_condSignal( &tor->cond );
711            }
712            continue;
713        }
714
715        /* Shuffle peers */
716        if( tor->peerCount > 1 )
717        {
718            peer = tor->peers[0];
719            memmove( &tor->peers[0], &tor->peers[1],
720                    ( tor->peerCount - 1 ) * sizeof( void * ) );
721            tor->peers[tor->peerCount - 1] = peer;
722        }
723
724        /* Receive/send messages */
725        for( i = 0; i < tor->peerCount; )
726        {
727            peer = tor->peers[i];
728
729            ret = tr_peerPulse( peer );
730            if( ret & TR_ERROR_IO_MASK )
731            {
732                tr_err( "Fatal error, stopping download (%d)", ret );
733                torrentStop( tor );
734                tor->error = ret;
735                snprintf( tor->errorString, sizeof( tor->errorString ),
736                          "%s", tr_errorString( ret ) );
737                break;
738            }
739            if( ret )
740            {
741                tr_peerDestroy( peer );
742                tor->peerCount--;
743                memmove( &tor->peers[i], &tor->peers[i+1],
744                         ( tor->peerCount - i ) * sizeof( void * ) );
745                continue;
746            }
747            i++;
748        }
749    }
750
751    tr_lockUnlock( &tor->lock );
752
753    if( tor->io )
754    {
755        tr_ioClose( tor->io ); tor->io = NULL;
756        tr_condSignal( &tor->cond );
757    }
758
759    tor->status = TR_STATUS_STOPPED;
760}
761
Note: See TracBrowser for help on using the repository browser.