source: trunk/daemon/daemon.c @ 13195

Last change on this file since 13195 was 13195, checked in by jordan, 9 years ago

(trunk) use base-10 units for network bandwidth (ie, speed) and disk sizes.

It looks like the Mac client is already doing this and it's clearly the trend in other apps as well. Even apt-get is using kB/s, ferchrissake... :)

Flame away.

  • Property svn:keywords set to Date Rev Author Id
File size: 20.0 KB
Line 
1/*
2 * This file Copyright (C) Mnemosyne LLC
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2
6 * as published by the Free Software Foundation.
7 *
8 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
9 *
10 * $Id: daemon.c 13195 2012-02-03 21:21:52Z jordan $
11 */
12
13#include <errno.h>
14#include <stdio.h> /* printf */
15#include <stdlib.h> /* exit, atoi */
16
17#include <fcntl.h> /* open */
18#include <signal.h>
19#ifdef HAVE_SYSLOG
20#include <syslog.h>
21#endif
22#include <unistd.h> /* daemon */
23
24#include <event2/buffer.h>
25
26#include <libtransmission/transmission.h>
27#include <libtransmission/bencode.h>
28#include <libtransmission/tr-getopt.h>
29#include <libtransmission/utils.h>
30#include <libtransmission/version.h>
31
32#include "watch.h"
33
34#define MY_NAME "transmission-daemon"
35
36#define PREF_KEY_DIR_WATCH          "watch-dir"
37#define PREF_KEY_DIR_WATCH_ENABLED  "watch-dir-enabled"
38#define PREF_KEY_PIDFILE            "pidfile"
39
40#define MEM_K 1024
41#define MEM_K_STR "KiB"
42#define MEM_M_STR "MiB"
43#define MEM_G_STR "GiB"
44#define MEM_T_STR "TiB"
45
46#define DISK_K 1000
47#define DISK_B_STR  "B"
48#define DISK_K_STR "kB"
49#define DISK_M_STR "MB"
50#define DISK_G_STR "GB"
51#define DISK_T_STR "TB"
52
53#define SPEED_K 1000
54#define SPEED_B_STR  "B/s"
55#define SPEED_K_STR "kB/s"
56#define SPEED_M_STR "MB/s"
57#define SPEED_G_STR "GB/s"
58#define SPEED_T_STR "TB/s"
59
60static bool paused = false;
61static bool closing = false;
62static tr_session * mySession = NULL;
63
64/***
65****  Config File
66***/
67
68static const char *
69getUsage( void )
70{
71    return "Transmission " LONG_VERSION_STRING
72           "  http://www.transmissionbt.com/\n"
73           "A fast and easy BitTorrent client\n"
74           "\n"
75           MY_NAME " is a headless Transmission session\n"
76           "that can be controlled via transmission-remote\n"
77           "or the web interface.\n"
78           "\n"
79           "Usage: " MY_NAME " [options]";
80}
81
82static const struct tr_option options[] =
83{
84
85    { 'a', "allowed", "Allowed IP addresses. (Default: " TR_DEFAULT_RPC_WHITELIST ")", "a", 1, "<list>" },
86    { 'b', "blocklist", "Enable peer blocklists", "b", 0, NULL },
87    { 'B', "no-blocklist", "Disable peer blocklists", "B", 0, NULL },
88    { 'c', "watch-dir", "Where to watch for new .torrent files", "c", 1, "<directory>" },
89    { 'C', "no-watch-dir", "Disable the watch-dir", "C", 0, NULL },
90    { 941, "incomplete-dir", "Where to store new torrents until they're complete", NULL, 1, "<directory>" },
91    { 942, "no-incomplete-dir", "Don't store incomplete torrents in a different location", NULL, 0, NULL },
92    { 'd', "dump-settings", "Dump the settings and exit", "d", 0, NULL },
93    { 'e', "logfile", "Dump the log messages to this filename", "e", 1, "<filename>" },
94    { 'f', "foreground", "Run in the foreground instead of daemonizing", "f", 0, NULL },
95    { 'g', "config-dir", "Where to look for configuration files", "g", 1, "<path>" },
96    { 'p', "port", "RPC port (Default: " TR_DEFAULT_RPC_PORT_STR ")", "p", 1, "<port>" },
97    { 't', "auth", "Require authentication", "t", 0, NULL },
98    { 'T', "no-auth", "Don't require authentication", "T", 0, NULL },
99    { 'u', "username", "Set username for authentication", "u", 1, "<username>" },
100    { 'v', "password", "Set password for authentication", "v", 1, "<password>" },
101    { 'V', "version", "Show version number and exit", "V", 0, NULL },
102    { 810, "log-error", "Show error messages", NULL, 0, NULL },
103    { 811, "log-info", "Show error and info messages", NULL, 0, NULL },
104    { 812, "log-debug", "Show error, info, and debug messages", NULL, 0, NULL },
105    { 'w', "download-dir", "Where to save downloaded data", "w", 1, "<path>" },
106    { 800, "paused", "Pause all torrents on startup", NULL, 0, NULL },
107    { 'o', "dht", "Enable distributed hash tables (DHT)", "o", 0, NULL },
108    { 'O', "no-dht", "Disable distributed hash tables (DHT)", "O", 0, NULL },
109    { 'y', "lpd", "Enable local peer discovery (LPD)", "y", 0, NULL },
110    { 'Y', "no-lpd", "Disable local peer discovery (LPD)", "Y", 0, NULL },
111    { 830, "utp", "Enable uTP for peer connections", NULL, 0, NULL },
112    { 831, "no-utp", "Disable uTP for peer connections", NULL, 0, NULL },
113    { 'P', "peerport", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "P", 1, "<port>" },
114    { 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", 0, NULL },
115    { 'M', "no-portmap", "Disable portmapping", "M", 0, NULL },
116    { 'L', "peerlimit-global", "Maximum overall number of peers (Default: " TR_DEFAULT_PEER_LIMIT_GLOBAL_STR ")", "L", 1, "<limit>" },
117    { 'l', "peerlimit-torrent", "Maximum number of peers per torrent (Default: " TR_DEFAULT_PEER_LIMIT_TORRENT_STR ")", "l", 1, "<limit>" },
118    { 910, "encryption-required",  "Encrypt all peer connections", "er", 0, NULL },
119    { 911, "encryption-preferred", "Prefer encrypted peer connections", "ep", 0, NULL },
120    { 912, "encryption-tolerated", "Prefer unencrypted peer connections", "et", 0, NULL },
121    { 'i', "bind-address-ipv4", "Where to listen for peer connections", "i", 1, "<ipv4 addr>" },
122    { 'I', "bind-address-ipv6", "Where to listen for peer connections", "I", 1, "<ipv6 addr>" },
123    { 'r', "rpc-bind-address", "Where to listen for RPC connections", "r", 1, "<ipv4 addr>" },
124    { 953, "global-seedratio", "All torrents, unless overridden by a per-torrent setting, should seed until a specific ratio", "gsr", 1, "ratio" },
125    { 954, "no-global-seedratio", "All torrents, unless overridden by a per-torrent setting, should seed regardless of ratio", "GSR", 0, NULL },
126    { 'x', "pid-file", "Enable PID file", "x", 1, "<pid-file>" },
127    { 0, NULL, NULL, NULL, 0, NULL }
128};
129
130static void
131showUsage( void )
132{
133    tr_getopt_usage( MY_NAME, getUsage( ), options );
134    exit( 0 );
135}
136
137static void
138gotsig( int sig )
139{
140    switch( sig )
141    {
142        case SIGHUP:
143        {
144            tr_benc settings;
145            const char * configDir = tr_sessionGetConfigDir( mySession );
146            tr_inf( "Reloading settings from \"%s\"", configDir );
147            tr_bencInitDict( &settings, 0 );
148            tr_bencDictAddBool( &settings, TR_PREFS_KEY_RPC_ENABLED, true );
149            tr_sessionLoadSettings( &settings, configDir, MY_NAME );
150            tr_sessionSet( mySession, &settings );
151            tr_bencFree( &settings );
152            tr_sessionReloadBlocklists( mySession );
153            break;
154        }
155
156        default:
157            closing = true;
158            break;
159    }
160}
161
162#if defined(WIN32)
163 #define USE_NO_DAEMON
164#elif !defined(HAVE_DAEMON) || defined(__UCLIBC__)
165 #define USE_TR_DAEMON
166#else
167 #define USE_OS_DAEMON
168#endif
169
170static int
171tr_daemon( int nochdir, int noclose )
172{
173#if defined(USE_OS_DAEMON)
174
175    return daemon( nochdir, noclose );
176
177#elif defined(USE_TR_DAEMON)
178
179    /* this is loosely based off of glibc's daemon() implementation
180     * http://sourceware.org/git/?p=glibc.git;a=blob_plain;f=misc/daemon.c */
181
182    switch( fork( ) ) {
183        case -1: return -1;
184        case 0: break;
185        default: _exit(0);
186    }
187
188    if( setsid( ) == -1 )
189        return -1;
190
191    if( !nochdir )
192        chdir( "/" );
193
194    if( !noclose ) {
195        int fd = open( "/dev/null", O_RDWR, 0 );
196        dup2( fd, STDIN_FILENO );
197        dup2( fd, STDOUT_FILENO );
198        dup2( fd, STDERR_FILENO );
199        close( fd );
200    }
201
202    return 0;
203
204#else /* USE_NO_DAEMON */
205    return 0;
206#endif
207}
208
209static const char*
210getConfigDir( int argc, const char ** argv )
211{
212    int c;
213    const char * configDir = NULL;
214    const char * optarg;
215    const int ind = tr_optind;
216
217    while(( c = tr_getopt( getUsage( ), argc, argv, options, &optarg ))) {
218        if( c == 'g' ) {
219            configDir = optarg;
220            break;
221        }
222    }
223
224    tr_optind = ind;
225
226    if( configDir == NULL )
227        configDir = tr_getDefaultConfigDir( MY_NAME );
228
229    return configDir;
230}
231
232static void
233onFileAdded( tr_session * session, const char * dir, const char * file )
234{
235    char * filename = tr_buildPath( dir, file, NULL );
236    tr_ctor * ctor = tr_ctorNew( session );
237    int err = tr_ctorSetMetainfoFromFile( ctor, filename );
238
239    if( !err )
240    {
241        tr_torrentNew( ctor, &err );
242
243        if( err == TR_PARSE_ERR )
244            tr_err( "Error parsing .torrent file \"%s\"", file );
245        else
246        {
247            bool trash = false;
248            int test = tr_ctorGetDeleteSource( ctor, &trash );
249
250            tr_inf( "Parsing .torrent file successful \"%s\"", file );
251
252            if( !test && trash )
253            {
254                tr_inf( "Deleting input .torrent file \"%s\"", file );
255                if( remove( filename ) )
256                    tr_err( "Error deleting .torrent file: %s", tr_strerror( errno ) );
257            }
258            else
259            {
260                char * new_filename = tr_strdup_printf( "%s.added", filename );
261                rename( filename, new_filename );
262                tr_free( new_filename );
263            }
264        }
265    }
266
267    tr_ctorFree( ctor );
268    tr_free( filename );
269}
270
271static void
272printMessage( FILE * logfile, int level, const char * name, const char * message, const char * file, int line )
273{
274    if( logfile != NULL )
275    {
276        char timestr[64];
277        tr_getLogTimeStr( timestr, sizeof( timestr ) );
278        if( name )
279            fprintf( logfile, "[%s] %s %s (%s:%d)\n", timestr, name, message, file, line );
280        else
281            fprintf( logfile, "[%s] %s (%s:%d)\n", timestr, message, file, line );
282    }
283#ifdef HAVE_SYSLOG
284    else /* daemon... write to syslog */
285    {
286        int priority;
287
288        /* figure out the syslog priority */
289        switch( level ) {
290            case TR_MSG_ERR: priority = LOG_ERR; break;
291            case TR_MSG_DBG: priority = LOG_DEBUG; break;
292            default: priority = LOG_INFO; break;
293        }
294
295        if( name )
296            syslog( priority, "%s %s (%s:%d)", name, message, file, line );
297        else
298            syslog( priority, "%s (%s:%d)", message, file, line );
299    }
300#endif
301}
302
303static void
304pumpLogMessages( FILE * logfile )
305{
306    const tr_msg_list * l;
307    tr_msg_list * list = tr_getQueuedMessages( );
308
309    for( l=list; l!=NULL; l=l->next )
310        printMessage( logfile, l->level, l->name, l->message, l->file, l->line );
311
312    if( logfile != NULL )
313        fflush( logfile );
314
315    tr_freeMessageList( list );
316}
317
318static tr_rpc_callback_status
319on_rpc_callback( tr_session            * session UNUSED,
320                 tr_rpc_callback_type    type,
321                 struct tr_torrent     * tor UNUSED,
322                 void                  * user_data UNUSED )
323{
324    if( type == TR_RPC_SESSION_CLOSE )
325        closing = true;
326    return TR_RPC_OK;
327}
328
329int
330main( int argc, char ** argv )
331{
332    int c;
333    const char * optarg;
334    tr_benc settings;
335    bool boolVal;
336    bool loaded;
337    bool foreground = false;
338    bool dumpSettings = false;
339    const char * configDir = NULL;
340    const char * pid_filename;
341    dtr_watchdir * watchdir = NULL;
342    FILE * logfile = NULL;
343    bool pidfile_created = false;
344
345    signal( SIGINT, gotsig );
346    signal( SIGTERM, gotsig );
347#ifndef WIN32
348    signal( SIGHUP, gotsig );
349#endif
350
351    /* load settings from defaults + config file */
352    tr_bencInitDict( &settings, 0 );
353    tr_bencDictAddBool( &settings, TR_PREFS_KEY_RPC_ENABLED, true );
354    configDir = getConfigDir( argc, (const char**)argv );
355    loaded = tr_sessionLoadSettings( &settings, configDir, MY_NAME );
356
357    /* overwrite settings from the comamndline */
358    tr_optind = 1;
359    while(( c = tr_getopt( getUsage(), argc, (const char**)argv, options, &optarg ))) {
360        switch( c ) {
361            case 'a': tr_bencDictAddStr( &settings, TR_PREFS_KEY_RPC_WHITELIST, optarg );
362                      tr_bencDictAddBool( &settings, TR_PREFS_KEY_RPC_WHITELIST_ENABLED, true );
363                      break;
364            case 'b': tr_bencDictAddBool( &settings, TR_PREFS_KEY_BLOCKLIST_ENABLED, true );
365                      break;
366            case 'B': tr_bencDictAddBool( &settings, TR_PREFS_KEY_BLOCKLIST_ENABLED, false );
367                      break;
368            case 'c': tr_bencDictAddStr( &settings, PREF_KEY_DIR_WATCH, optarg );
369                      tr_bencDictAddBool( &settings, PREF_KEY_DIR_WATCH_ENABLED, true );
370                      break;
371            case 'C': tr_bencDictAddBool( &settings, PREF_KEY_DIR_WATCH_ENABLED, false );
372                      break;
373            case 941: tr_bencDictAddStr( &settings, TR_PREFS_KEY_INCOMPLETE_DIR, optarg );
374                      tr_bencDictAddBool( &settings, TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED, true );
375                      break;
376            case 942: tr_bencDictAddBool( &settings, TR_PREFS_KEY_INCOMPLETE_DIR_ENABLED, false );
377                      break;
378            case 'd': dumpSettings = true;
379                      break;
380            case 'e': logfile = fopen( optarg, "a+" );
381                      if( logfile == NULL )
382                          fprintf( stderr, "Couldn't open \"%s\": %s\n", optarg, tr_strerror( errno ) );
383                      break;
384            case 'f': foreground = true;
385                      break;
386            case 'g': /* handled above */
387                      break;
388            case 'V': /* version */
389                      fprintf(stderr, "%s %s\n", MY_NAME, LONG_VERSION_STRING);
390                      exit( 0 );
391            case 'o': tr_bencDictAddBool( &settings, TR_PREFS_KEY_DHT_ENABLED, true );
392                      break;
393            case 'O': tr_bencDictAddBool( &settings, TR_PREFS_KEY_DHT_ENABLED, false );
394                      break;
395            case 'p': tr_bencDictAddInt( &settings, TR_PREFS_KEY_RPC_PORT, atoi( optarg ) );
396                      break;
397            case 't': tr_bencDictAddBool( &settings, TR_PREFS_KEY_RPC_AUTH_REQUIRED, true );
398                      break;
399            case 'T': tr_bencDictAddBool( &settings, TR_PREFS_KEY_RPC_AUTH_REQUIRED, false );
400                      break;
401            case 'u': tr_bencDictAddStr( &settings, TR_PREFS_KEY_RPC_USERNAME, optarg );
402                      break;
403            case 'v': tr_bencDictAddStr( &settings, TR_PREFS_KEY_RPC_PASSWORD, optarg );
404                      break;
405            case 'w': tr_bencDictAddStr( &settings, TR_PREFS_KEY_DOWNLOAD_DIR, optarg );
406                      break;
407            case 'P': tr_bencDictAddInt( &settings, TR_PREFS_KEY_PEER_PORT, atoi( optarg ) );
408                      break;
409            case 'm': tr_bencDictAddBool( &settings, TR_PREFS_KEY_PORT_FORWARDING, true );
410                      break;
411            case 'M': tr_bencDictAddBool( &settings, TR_PREFS_KEY_PORT_FORWARDING, false );
412                      break;
413            case 'L': tr_bencDictAddInt( &settings, TR_PREFS_KEY_PEER_LIMIT_GLOBAL, atoi( optarg ) );
414                      break;
415            case 'l': tr_bencDictAddInt( &settings, TR_PREFS_KEY_PEER_LIMIT_TORRENT, atoi( optarg ) );
416                      break;
417            case 800: paused = true;
418                      break;
419            case 910: tr_bencDictAddInt( &settings, TR_PREFS_KEY_ENCRYPTION, TR_ENCRYPTION_REQUIRED );
420                      break;
421            case 911: tr_bencDictAddInt( &settings, TR_PREFS_KEY_ENCRYPTION, TR_ENCRYPTION_PREFERRED );
422                      break;
423            case 912: tr_bencDictAddInt( &settings, TR_PREFS_KEY_ENCRYPTION, TR_CLEAR_PREFERRED );
424                      break;
425            case 'i': tr_bencDictAddStr( &settings, TR_PREFS_KEY_BIND_ADDRESS_IPV4, optarg );
426                      break;
427            case 'I': tr_bencDictAddStr( &settings, TR_PREFS_KEY_BIND_ADDRESS_IPV6, optarg );
428                      break;
429            case 'r': tr_bencDictAddStr( &settings, TR_PREFS_KEY_RPC_BIND_ADDRESS, optarg );
430                      break;
431            case 953: tr_bencDictAddReal( &settings, TR_PREFS_KEY_RATIO, atof(optarg) );
432                      tr_bencDictAddBool( &settings, TR_PREFS_KEY_RATIO_ENABLED, true );
433                      break;
434            case 954: tr_bencDictAddBool( &settings, TR_PREFS_KEY_RATIO_ENABLED, false );
435                      break;
436            case 'x': tr_bencDictAddStr( &settings, PREF_KEY_PIDFILE, optarg );
437                      break;
438            case 'y': tr_bencDictAddBool( &settings, TR_PREFS_KEY_LPD_ENABLED, true );
439                      break;
440            case 'Y': tr_bencDictAddBool( &settings, TR_PREFS_KEY_LPD_ENABLED, false );
441                      break;
442            case 810: tr_bencDictAddInt( &settings,  TR_PREFS_KEY_MSGLEVEL, TR_MSG_ERR );
443                      break;
444            case 811: tr_bencDictAddInt( &settings,  TR_PREFS_KEY_MSGLEVEL, TR_MSG_INF );
445                      break;
446            case 812: tr_bencDictAddInt( &settings,  TR_PREFS_KEY_MSGLEVEL, TR_MSG_DBG );
447                      break;
448            case 830: tr_bencDictAddBool( &settings, TR_PREFS_KEY_UTP_ENABLED, true );
449                      break;
450            case 831: tr_bencDictAddBool( &settings, TR_PREFS_KEY_UTP_ENABLED, false );
451                      break;
452            default:  showUsage( );
453                      break;
454        }
455    }
456
457    if( foreground && !logfile )
458        logfile = stderr;
459
460    if( !loaded )
461    {
462        printMessage( logfile, TR_MSG_ERR, MY_NAME, "Error loading config file -- exiting.", __FILE__, __LINE__ );
463        return -1;
464    }
465
466    if( dumpSettings )
467    {
468        char * str = tr_bencToStr( &settings, TR_FMT_JSON, NULL );
469        fprintf( stderr, "%s", str );
470        tr_free( str );
471        return 0;
472    }
473
474    if( !foreground && tr_daemon( true, false ) < 0 )
475    {
476        char buf[256];
477        tr_snprintf( buf, sizeof( buf ), "Failed to daemonize: %s", tr_strerror( errno ) );
478        printMessage( logfile, TR_MSG_ERR, MY_NAME, buf, __FILE__, __LINE__ );
479        exit( 1 );
480    }
481
482    /* start the session */
483    tr_formatter_mem_init( MEM_K, MEM_K_STR, MEM_M_STR, MEM_G_STR, MEM_T_STR );
484    tr_formatter_size_init( DISK_K, DISK_K_STR, DISK_M_STR, DISK_G_STR, DISK_T_STR );
485    tr_formatter_speed_init( SPEED_K, SPEED_K_STR, SPEED_M_STR, SPEED_G_STR, SPEED_T_STR );
486    mySession = tr_sessionInit( "daemon", configDir, true, &settings );
487    tr_sessionSetRPCCallback( mySession, on_rpc_callback, NULL );
488    tr_ninf( NULL, "Using settings from \"%s\"", configDir );
489    tr_sessionSaveSettings( mySession, configDir, &settings );
490
491    pid_filename = NULL;
492    tr_bencDictFindStr( &settings, PREF_KEY_PIDFILE, &pid_filename );
493    if( pid_filename && *pid_filename )
494    {
495        FILE * fp = fopen( pid_filename, "w+" );
496        if( fp != NULL )
497        {
498            fprintf( fp, "%d", (int)getpid() );
499            fclose( fp );
500            tr_inf( "Saved pidfile \"%s\"", pid_filename );
501            pidfile_created = true;
502        }
503        else
504            tr_err( "Unable to save pidfile \"%s\": %s", pid_filename, tr_strerror( errno ) );
505    }
506
507    if( tr_bencDictFindBool( &settings, TR_PREFS_KEY_RPC_AUTH_REQUIRED, &boolVal ) && boolVal )
508        tr_ninf( MY_NAME, "requiring authentication" );
509
510    /* maybe add a watchdir */
511    {
512        const char * dir;
513
514        if( tr_bencDictFindBool( &settings, PREF_KEY_DIR_WATCH_ENABLED, &boolVal )
515            && boolVal
516            && tr_bencDictFindStr( &settings, PREF_KEY_DIR_WATCH, &dir )
517            && dir
518            && *dir )
519        {
520            tr_inf( "Watching \"%s\" for new .torrent files", dir );
521            watchdir = dtr_watchdir_new( mySession, dir, onFileAdded );
522        }
523    }
524
525    /* load the torrents */
526    {
527        tr_torrent ** torrents;
528        tr_ctor * ctor = tr_ctorNew( mySession );
529        if( paused )
530            tr_ctorSetPaused( ctor, TR_FORCE, true );
531        torrents = tr_sessionLoadTorrents( mySession, ctor, NULL );
532        tr_free( torrents );
533        tr_ctorFree( ctor );
534    }
535
536#ifdef HAVE_SYSLOG
537    if( !foreground )
538        openlog( MY_NAME, LOG_CONS|LOG_PID, LOG_DAEMON );
539#endif
540
541    while( !closing ) {
542        tr_wait_msec( 1000 ); /* sleep one second */
543        dtr_watchdir_update( watchdir );
544        pumpLogMessages( logfile );
545    }
546
547    printf( "Closing transmission session..." );
548    tr_sessionSaveSettings( mySession, configDir, &settings );
549    dtr_watchdir_free( watchdir );
550    tr_sessionClose( mySession );
551    pumpLogMessages( logfile );
552    printf( " done.\n" );
553
554    /* shutdown */
555#if HAVE_SYSLOG
556    if( !foreground )
557    {
558        syslog( LOG_INFO, "%s", "Closing session" );
559        closelog( );
560    }
561#endif
562
563    /* cleanup */
564    if( pidfile_created )
565        remove( pid_filename );
566    tr_bencFree( &settings );
567    return 0;
568}
Note: See TracBrowser for help on using the repository browser.