source: branches/file_selection/libtransmission/peer.c @ 2064

Last change on this file since 2064 was 2064, checked in by charles, 15 years ago

Attempt fixing the partial download seed problem.

We need to not tell trackers that we're seeders when we're not, so I've
added a new tr_torrent state TR_STATE_DONE (see transmission.h:489)
to indicate we're not seeders, but we're not asking for anything either.
The GTK client draws SEED as "Seeding; Uploading to X of Y peers"

and DONE as "Uploading to X of Y peers".

  • Property svn:keywords set to Date Rev Author Id
File size: 22.3 KB
Line 
1/******************************************************************************
2 * $Id: peer.c 2064 2007-06-13 08:38:42Z charles $
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 "peertree.h"
27
28#define PERCENT_PEER_WANTED     25      /* Percent before we start relax peers min activeness */
29#define MIN_UPLOAD_IDLE         60000   /* In high peer situations we wait only 1 min
30                                            until dropping peers for idling */
31#define MAX_UPLOAD_IDLE         240000  /* In low peer situations we wait the
32                                            4 mins until dropping peers for idling */
33#define MIN_KEEP_ALIVE          180000  /* In high peer situations we wait only 3 min
34                                            without a keep-alive */
35#define MAX_KEEP_ALIVE          360000  /* In low peer situations we wait the
36                                            6 mins without a keep-alive */
37#define MIN_CON_TIMEOUT         8000    /* Time to timeout connecting to peer,
38                                            during low peer situations */
39#define MAX_CON_TIMEOUT         30000   /* Time to timeout connecting to peer,
40                                            during high peer situations */
41#define MAX_REQUEST_COUNT       32
42#define OUR_REQUEST_COUNT       8  /* TODO: we should detect if we are on a
43                                      high-speed network and adapt */
44#define PEX_PEER_CUTOFF         50 /* only try to add new peers from pex if
45                                      we have fewer existing peers than this */
46#define PEX_INTERVAL            60 /* don't send pex messages more frequently
47                                      than PEX_INTERVAL +
48                                      rand( PEX_INTERVAL / 10 ) seconds */
49#define PEER_SUPPORTS_EXTENDED_MESSAGES( bits ) ( (bits)[5] & 0x10 )
50#define PEER_SUPPORTS_AZUREUS_PROTOCOL( bits )  ( (bits)[0] & 0x80 )
51
52#define PEER_MSG_CHOKE          0
53#define PEER_MSG_UNCHOKE        1
54#define PEER_MSG_INTERESTED     2
55#define PEER_MSG_UNINTERESTED   3
56#define PEER_MSG_HAVE           4
57#define PEER_MSG_BITFIELD       5
58#define PEER_MSG_REQUEST        6
59#define PEER_MSG_PIECE          7
60#define PEER_MSG_CANCEL         8
61#define PEER_MSG_PORT           9
62#define PEER_MSG_EXTENDED       20
63
64typedef struct tr_request_s
65{
66    int index;
67    int begin;
68    int length;
69
70} tr_request_t;
71
72struct tr_peer_s
73{
74    tr_torrent_t      * tor;
75
76    struct in_addr      addr;
77    in_port_t           port;  /* peer's listening port, 0 if not known */
78
79#define PEER_STATUS_IDLE        1 /* Need to connect */
80#define PEER_STATUS_CONNECTING  2 /* Trying to send handshake */
81#define PEER_STATUS_HANDSHAKE   3 /* Waiting for peer's handshake */
82#define PEER_STATUS_AZ_GIVER    4 /* Sending Azureus handshake */
83#define PEER_STATUS_AZ_RECEIVER 5 /* Receiving Azureus handshake */
84#define PEER_STATUS_CONNECTED   6 /* Got peer's handshake */
85    int                 status;
86    int                 socket;
87    char                from;
88    char                private;
89    char                azproto;  /* azureus peer protocol is being used */
90    uint64_t            date;
91    uint64_t            keepAlive;
92
93#define EXTENDED_NOT_SUPPORTED   0 /* extended messages not supported */
94#define EXTENDED_SUPPORTED       1 /* waiting to send extended handshake */
95#define EXTENDED_HANDSHAKE       2 /* sent extended handshake */
96    uint8_t             extStatus;
97    uint8_t             pexStatus;   /* peer's ut_pex id, 0 if not supported */
98    uint64_t            lastPex;     /* time when last pex packet was sent */
99    int                 advertisedPort; /* listening port we last told peer */
100    tr_peertree_t       sentPeers;
101
102    char                amChoking;
103    char                amInterested;
104    char                peerChoking;
105    char                peerInterested;
106
107    int                 optimistic;
108    uint64_t            lastChoke;
109
110    uint8_t             id[TR_ID_LEN];
111
112    /* The pieces that the peer has */
113    tr_bitfield_t     * bitfield;
114    int                 pieceCount;
115    float               progress;
116
117    int                 goodPcs;
118    int                 badPcs;
119    int                 banned;
120    /* The pieces that the peer is contributing to */
121    tr_bitfield_t     * blamefield;
122    /* The bad pieces that the peer has contributed to */
123    tr_bitfield_t     * banfield;
124
125    uint8_t           * buf;
126    int                 size;
127    int                 pos;
128
129    uint8_t           * outMessages;
130    int                 outMessagesSize;
131    int                 outMessagesPos;
132    uint8_t             outBlock[25+16384];
133    int                 outBlockSize;
134    int                 outBlockLoaded;
135    int                 outBlockSending;
136
137    int                 inRequestCount;
138    tr_request_t        inRequests[OUR_REQUEST_COUNT];
139    int                 inIndex;
140    int                 inBegin;
141    int                 inLength;
142    uint64_t            inTotal;
143
144    int                 outRequestCount;
145    tr_request_t        outRequests[MAX_REQUEST_COUNT];
146    uint64_t            outTotal;
147    uint64_t            outDate;
148
149    tr_ratecontrol_t  * download;
150    tr_ratecontrol_t  * upload;
151
152    char              * client;
153};
154
155#define peer_dbg( a... ) __peer_dbg( peer, ## a )
156static void __peer_dbg( tr_peer_t * peer, char * msg, ... ) PRINTF( 2, 3 );
157
158static void __peer_dbg( tr_peer_t * peer, char * msg, ... )
159{
160    char    string[256];
161    va_list args;
162
163    va_start( args, msg );
164    snprintf( string, sizeof string, "%08x:%04x ",
165             (uint32_t) peer->addr.s_addr, peer->port );
166    vsnprintf( &string[14], sizeof( string ) - 14, msg, args );
167    va_end( args ); 
168
169    tr_dbg( "%s", string );
170}
171
172#include "peerext.h"
173#include "peeraz.h"
174#include "peermessages.h"
175#include "peerutils.h"
176#include "peerparse.h"
177
178/***********************************************************************
179 * tr_peerInit
180 ***********************************************************************
181 * Initializes a new peer.
182 **********************************************************************/
183tr_peer_t * tr_peerInit( struct in_addr addr, in_port_t port, int s, int from )
184{
185    tr_peer_t * peer = peerInit();
186
187    assert( 0 <= from && TR_PEER_FROM__MAX > from );
188
189    peer->socket = s;
190    peer->addr = addr;
191    peer->port = port;
192    peer->from = from;
193    if( s >= 0 )
194    {
195        assert( TR_PEER_FROM_INCOMING == from );
196        peer->status = PEER_STATUS_CONNECTING;
197    }
198    else
199    {
200        peer->status = PEER_STATUS_IDLE;
201    }
202
203    return peer;
204}
205
206void tr_peerDestroy( tr_peer_t * peer )
207{
208    tr_torrent_t * tor = peer->tor;
209    tr_request_t * r;
210    int i, block;
211
212    peertreeFree( &peer->sentPeers );
213    for( i = 0; i < peer->inRequestCount; i++ )
214    {
215        r = &peer->inRequests[i];
216        block = tr_block( r->index, r->begin );
217        tr_cpDownloaderRem( tor->completion, block );
218    }
219    tr_bitfieldFree( peer->bitfield );
220    tr_bitfieldFree( peer->blamefield );
221    tr_bitfieldFree( peer->banfield );
222    if( peer->buf )
223    {
224        free( peer->buf );
225    }
226    if( peer->outMessages )
227    {
228        free( peer->outMessages );
229    }
230    if( peer->status > PEER_STATUS_IDLE )
231    {
232        tr_netClose( peer->socket );
233    }
234    tr_rcClose( peer->download );
235    tr_rcClose( peer->upload );
236    free( peer->client );
237    free( peer );
238}
239
240const char *
241tr_peerClient( tr_peer_t * peer )
242{
243    if( PEER_STATUS_HANDSHAKE >= peer->status )
244    {
245        return "not connected";
246    }
247
248    if( NULL == peer->client )
249    {
250        peer->client = tr_clientForId( peer->id );
251    }
252
253    return peer->client;
254}
255
256void tr_peerSetPrivate( tr_peer_t * peer, int private )
257{
258    if( peer->private == private )
259    {
260        return;
261    }
262
263    peer->private = private;
264
265    if( !private )
266    {
267        peer->lastPex = 0;
268    }
269
270    if( EXTENDED_HANDSHAKE == peer->extStatus )
271    {
272        sendExtended( peer->tor, peer, EXTENDED_HANDSHAKE_ID );
273    }
274}
275
276void tr_peerSetTorrent( tr_peer_t * peer, tr_torrent_t * tor )
277{
278    peer->tor = tor;
279}
280
281/***********************************************************************
282 * tr_peerRead
283 ***********************************************************************
284 *
285 **********************************************************************/
286int tr_peerRead( tr_peer_t * peer )
287{
288    tr_torrent_t * tor = peer->tor;
289    int ret;
290    uint64_t date;
291
292    /* Try to read */
293    for( ;; )
294    {
295        if( tor )
296        {
297            if( tor->customDownloadLimit
298                ? !tr_rcCanTransfer( tor->download )
299                : !tr_rcCanTransfer( tor->handle->download ) )
300            {
301                break;
302            }
303        }
304
305        if( peer->size < 1 )
306        {
307            peer->size = 1024;
308            peer->buf  = malloc( peer->size );
309        }
310        else if( peer->pos >= peer->size )
311        {
312            peer->size *= 2;
313            peer->buf   = realloc( peer->buf, peer->size );
314        }
315        /* Never read more than 1K each time, otherwise the rate
316           control is no use */
317        ret = tr_netRecv( peer->socket, &peer->buf[peer->pos],
318                          MIN( 1024, peer->size - peer->pos ) );
319        if( ret & TR_NET_CLOSE )
320        {
321            peer_dbg( "connection closed" );
322            return TR_ERROR;
323        }
324        else if( ret & TR_NET_BLOCK )
325        {
326            break;
327        }
328        date        = tr_date();
329        peer->date  = date;
330        peer->pos  += ret;
331        if( NULL != tor )
332        {
333            tr_rcTransferred( peer->download, ret );
334            tr_rcTransferred( tor->download, ret );
335            if ( !tor->customDownloadLimit )
336            {
337                tr_rcTransferred( tor->handle->download, ret );
338            }
339           
340            if( tr_peerAmInterested( peer ) && !tr_peerIsChoking( peer ) )
341            {
342                tor->activityDate = date;
343            }
344           
345            if( ( ret = parseBuf( tor, peer ) ) )
346            {
347                return ret;
348            }
349        }
350        else
351        {
352            if( ( ret = parseBufHeader( peer ) ) )
353            {
354                return ret;
355            }
356        }
357    }
358
359    return TR_OK;
360}
361
362uint64_t tr_peerDate( tr_peer_t * peer )
363{
364    return peer->date;
365}
366
367/***********************************************************************
368 * tr_peerId
369 ***********************************************************************
370 *
371 **********************************************************************/
372uint8_t * tr_peerId( tr_peer_t * peer )
373{
374    return & peer->id[0];
375}
376
377/***********************************************************************
378 * tr_peerAddress
379 ***********************************************************************
380 *
381 **********************************************************************/
382struct in_addr * tr_peerAddress( tr_peer_t * peer )
383{
384    return &peer->addr;
385}
386
387/***********************************************************************
388 * tr_peerHash
389 ***********************************************************************
390 *
391 **********************************************************************/
392uint8_t * tr_peerHash( tr_peer_t * peer )
393{
394    return parseBufHash( peer );
395}
396
397/***********************************************************************
398 * tr_peerPulse
399 ***********************************************************************
400 *
401 **********************************************************************/
402int tr_peerPulse( tr_peer_t * peer )
403{
404    tr_torrent_t * tor = peer->tor;
405    int ret, size;
406    uint8_t * p;
407    uint64_t date;
408
409    if( ( ret = checkPeer( peer ) ) )
410    {
411        return ret;
412    }
413
414    /* Connect */
415    if( PEER_STATUS_IDLE == peer->status )
416    {
417        peer->socket = tr_netOpenTCP( peer->addr, peer->port, 0 );
418        if( peer->socket < 0 )
419        {
420            return TR_ERROR;
421        }
422        peer->status = PEER_STATUS_CONNECTING;
423    }
424   
425    /* Disconnect if seeder and torrent is seeding */
426    if( peer->tor->status == TR_STATUS_SEED && peer->progress >= 1.0 )
427    {
428        return TR_ERROR;
429    }
430
431    /* Try to send handshake */
432    if( PEER_STATUS_CONNECTING == peer->status )
433    {
434        uint8_t buf[68];
435        tr_info_t * inf = &tor->info;
436
437        buf[0] = 19;
438        memcpy( &buf[1], "BitTorrent protocol", 19 );
439        memset( &buf[20], 0, 8 );
440        buf[20] = 0x80;         /* azureus protocol */
441        buf[25] = 0x10;         /* extended messages */
442        memcpy( &buf[28], inf->hash, 20 );
443        memcpy( &buf[48], tor->id, 20 );
444
445        switch( tr_netSend( peer->socket, buf, 68 ) )
446        {
447            case 68:
448                peer_dbg( "SEND handshake" );
449                peer->status = PEER_STATUS_HANDSHAKE;
450                break;
451            case TR_NET_BLOCK:
452                break;
453            default:
454                peer_dbg( "connection closed" );
455                return TR_ERROR;
456        }
457    }
458    if( peer->status < PEER_STATUS_HANDSHAKE )
459    {
460        /* Nothing more we can do till we sent the handshake */
461        return TR_OK;
462    }
463
464    /* Read incoming messages */
465    if( ( ret = tr_peerRead( peer ) ) )
466    {
467        return ret;
468    }
469
470    /* Try to send Azureus handshake */
471    if( PEER_STATUS_AZ_GIVER == peer->status )
472    {
473        switch( sendAZHandshake( tor, peer ) )
474        {
475            case TR_NET_BLOCK:
476                break;
477            case TR_NET_CLOSE:
478                peer_dbg( "connection closed" );
479                return TR_ERROR;
480            default:
481                peer->status = PEER_STATUS_AZ_RECEIVER;
482                break;
483        }
484    }
485
486    if( peer->status < PEER_STATUS_CONNECTED )
487    {
488        /* Nothing more we can do till we got the other guy's handshake */
489        return TR_OK;
490    }
491
492    /* Try to write */
493writeBegin:
494
495    /* Send all smaller messages regardless of the upload cap */
496    while( ( p = messagesPending( peer, &size ) ) )
497    {
498        ret = tr_netSend( peer->socket, p, size );
499        if( ret & TR_NET_CLOSE )
500        {
501            return TR_ERROR;
502        }
503        else if( ret & TR_NET_BLOCK )
504        {
505            goto writeEnd;
506        }
507        messagesSent( peer, ret );
508    }
509
510    /* Send pieces if we can */
511    while( ( p = blockPending( tor, peer, &size ) ) )
512    {
513        if( tor->customUploadLimit
514            ? !tr_rcCanTransfer( tor->upload )
515            : !tr_rcCanTransfer( tor->handle->upload ) )
516        {
517            break;
518        }
519
520        ret = tr_netSend( peer->socket, p, size );
521        if( ret & TR_NET_CLOSE )
522        {
523            return TR_ERROR;
524        }
525        else if( ret & TR_NET_BLOCK )
526        {
527            break;
528        }
529
530        blockSent( peer, ret );
531        tr_rcTransferred( peer->upload, ret );
532        tr_rcTransferred( tor->upload, ret );
533        if ( !tor->customUploadLimit )
534        {
535            tr_rcTransferred( tor->handle->upload, ret );
536        }
537
538        tor->uploadedCur += ret;
539        peer->outTotal   += ret;
540       
541        date              = tr_date();
542        peer->outDate     = date;
543       
544        if( !tr_peerAmChoking( peer ) )
545        {
546            tor->activityDate = date;
547        }
548
549        /* In case this block is done, you may have messages
550           pending. Send them before we start the next block */
551        goto writeBegin;
552    }
553writeEnd:
554
555    /* Ask for a block whenever possible */
556    if( tr_cpGetState( tor->completion ) == CP_INCOMPLETE
557        && !peer->amInterested
558        && tor->peerCount > TR_MAX_PEER_COUNT - 2 )
559    {
560        /* This peer is no use to us, and it seems there are
561           more */
562        peer_dbg( "not interesting" );
563        return TR_ERROR;
564    }
565
566    if(     peer->amInterested
567        && !peer->peerChoking
568        && !peer->banned
569        &&  peer->inRequestCount < OUR_REQUEST_COUNT )
570    {
571        int i;
572        int poolSize=0, endgame=0;
573        int * pool = getPreferredPieces ( tor, peer, &poolSize, &endgame );
574
575        /* TODO: add some asserts to see if this bitfield is really necessary */
576        tr_bitfield_t * blocksAlreadyRequested = tr_bitfieldNew( 1 + tor->info.totalSize / tor->blockSize );
577        for( i=0; i<peer->inRequestCount; ++i) {
578            const tr_request_t * r = &peer->inRequests[i];
579            const int block = tr_block( r->index, r->begin );
580            tr_bitfieldAdd( blocksAlreadyRequested, block );
581        }
582
583        for( i=0; i<poolSize && peer->inRequestCount<OUR_REQUEST_COUNT;  )
584        {
585            int unused;
586            const int piece = pool[i];
587            const int block = endgame
588                ? tr_cpMostMissingBlockInPiece( tor->completion, piece, &unused)
589                : tr_cpMissingBlockInPiece ( tor->completion, piece );
590
591            if( block>=0 && (endgame || !tr_bitfieldHas( blocksAlreadyRequested, block ) ) )
592            {
593                tr_bitfieldAdd( blocksAlreadyRequested, block );
594                sendRequest( tor, peer, block );
595            }
596            else ++i;
597        }
598
599        tr_bitfieldFree( blocksAlreadyRequested );
600        free( pool );
601    }
602
603    return TR_OK;
604}
605
606/***********************************************************************
607 * tr_peerIsConnected
608 ***********************************************************************
609 *
610 **********************************************************************/
611int tr_peerIsConnected( tr_peer_t * peer )
612{
613    return PEER_STATUS_CONNECTED == peer->status;
614}
615
616/***********************************************************************
617 * tr_peerIsIncoming
618 ***********************************************************************
619 *
620 **********************************************************************/
621int tr_peerIsFrom( tr_peer_t * peer )
622{
623    return peer->from;
624}
625
626int tr_peerAmChoking( tr_peer_t * peer )
627{
628    return peer->amChoking;
629}
630int tr_peerAmInterested( tr_peer_t * peer )
631{
632    return peer->amInterested;
633}
634int tr_peerIsChoking( tr_peer_t * peer )
635{
636    return peer->peerChoking;
637}
638int tr_peerIsInterested( tr_peer_t * peer )
639{
640    return peer->peerInterested;
641}
642
643/***********************************************************************
644 * tr_peerProgress
645 ***********************************************************************
646 *
647 **********************************************************************/
648float tr_peerProgress( tr_peer_t * peer )
649{
650    return peer->progress;
651}
652
653/***********************************************************************
654 * tr_peerPort
655 ***********************************************************************
656 * Returns peer's listening port in host byte order
657 **********************************************************************/
658int tr_peerPort( tr_peer_t * peer )
659{
660    return ntohs( peer->port );
661}
662
663/***********************************************************************
664 * tr_peerBitfield
665 ***********************************************************************
666 *
667 **********************************************************************/
668tr_bitfield_t * tr_peerBitfield( tr_peer_t * peer )
669{
670    return peer->bitfield;
671}
672
673float tr_peerDownloadRate( tr_peer_t * peer )
674{
675    return tr_rcRate( peer->download );
676}
677
678float tr_peerUploadRate( tr_peer_t * peer )
679{
680    return tr_rcRate( peer->upload );
681}
682
683void tr_peerChoke( tr_peer_t * peer )
684{
685    sendChoke( peer, 1 );
686    peer->lastChoke = tr_date();
687}
688
689void tr_peerUnchoke( tr_peer_t * peer )
690{
691    sendChoke( peer, 0 );
692    peer->lastChoke = tr_date();
693}
694
695uint64_t tr_peerLastChoke( tr_peer_t * peer )
696{
697    return peer->lastChoke;
698}
699
700void tr_peerSetOptimistic( tr_peer_t * peer, int o )
701{
702    peer->optimistic = o;
703}
704
705int tr_peerIsOptimistic( tr_peer_t * peer )
706{
707    return peer->optimistic;
708}
709
710static inline int peerIsBad( tr_peer_t * peer )
711{
712    return ( peer->badPcs > 4 + 2 * peer->goodPcs );
713}
714
715static inline int peerIsGood( tr_peer_t * peer )
716{
717    return ( peer->goodPcs > 3 * peer->badPcs );
718}
719
720void tr_peerBlame( tr_peer_t * peer, int piece, int success )
721{
722    tr_torrent_t * tor = peer->tor;
723
724    if( !peer->blamefield || !tr_bitfieldHas( peer->blamefield, piece ) )
725    {
726        return;
727    }
728
729    if( success )
730    {
731        peer->goodPcs++;
732
733        if( peer->banfield && peerIsGood( peer ) )
734        {
735            /* Assume the peer wasn't responsible for the bad pieces
736               we was banned for */
737            tr_bitfieldClear( peer->banfield );
738        }
739    }
740    else
741    {
742        peer->badPcs++;
743
744        /* Ban the peer for this piece */
745        if( !peer->banfield )
746        {
747            peer->banfield = tr_bitfieldNew( tor->info.pieceCount );
748        }
749        tr_bitfieldAdd( peer->banfield, piece );
750
751        if( peerIsBad( peer ) )
752        {
753            /* Full ban */
754            peer_dbg( "banned (%d / %d)", peer->goodPcs, peer->badPcs );
755            peer->banned = 1;
756            peer->peerInterested = 0;
757        }
758    }
759    tr_bitfieldRem( peer->blamefield, piece );
760}
761
762int tr_peerGetConnectable( const tr_torrent_t * tor, uint8_t ** _buf )
763{
764    int count = 0;
765    uint8_t * buf;
766    tr_peer_t * peer;
767    int i;
768
769    if( tor->peerCount < 1 )
770    {
771        *_buf = NULL;
772        return 0;
773    }
774
775    buf = malloc( 6 * tor->peerCount );
776    for( i = 0; i < tor->peerCount; i++ )
777    {
778        peer = tor->peers[i];
779
780        /* Skip peers with no known listening port */
781        if( 0 == peer->port )
782            continue;
783
784        memcpy( &buf[count*6], &peer->addr, 4 );
785        memcpy( &buf[count*6+4], &peer->port, 2 );
786        count++;
787    }
788
789    if( count < 1 )
790    {
791        free( buf ); buf = NULL;
792    }
793    *_buf = buf;
794
795    return count * 6;
796}
Note: See TracBrowser for help on using the repository browser.