2008-05-18 16:44:30 +00:00
|
|
|
/*
|
|
|
|
* This file Copyright (C) 2008 Charles Kerr <charles@rebelbase.com>
|
2007-04-18 16:39:10 +00:00
|
|
|
*
|
2008-05-18 16:44:30 +00:00
|
|
|
* This file is licensed by the GPL version 2. Works owned by the
|
|
|
|
* Transmission project are granted a special exemption to clause 2(b)
|
|
|
|
* so that the bulk of its code can remain under the MIT license.
|
|
|
|
* This exemption does not extend to derived works not owned by
|
|
|
|
* the Transmission project.
|
2007-04-18 16:39:10 +00:00
|
|
|
*
|
2008-05-19 16:16:38 +00:00
|
|
|
* $Id$
|
2008-05-18 16:44:30 +00:00
|
|
|
*/
|
|
|
|
|
2007-04-18 16:39:10 +00:00
|
|
|
#include <assert.h>
|
|
|
|
#include <errno.h>
|
2008-05-18 16:44:30 +00:00
|
|
|
#include <stdio.h> /* printf */
|
|
|
|
#include <stdlib.h> /* exit, atoi */
|
|
|
|
#include <string.h> /* strcmp */
|
|
|
|
|
|
|
|
#include <fcntl.h> /* open */
|
2007-04-18 16:39:10 +00:00
|
|
|
#include <getopt.h>
|
|
|
|
#include <signal.h>
|
2008-05-18 16:44:30 +00:00
|
|
|
#include <unistd.h> /* daemon, getcwd */
|
|
|
|
|
|
|
|
#include <libtransmission/transmission.h>
|
|
|
|
#include <libtransmission/bencode.h>
|
|
|
|
#include <libtransmission/rpc.h>
|
2008-05-22 19:24:11 +00:00
|
|
|
#include <libtransmission/utils.h>
|
2007-07-18 23:04:26 +00:00
|
|
|
#include <libtransmission/version.h>
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
#define MY_NAME "transmission-daemon"
|
2007-04-18 16:39:10 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
static int closing = FALSE;
|
2008-05-22 19:24:11 +00:00
|
|
|
static tr_handle * mySession;
|
|
|
|
static char myConfigFilename[MAX_PATH_LENGTH];
|
2007-04-18 16:39:10 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
static void
|
|
|
|
saveState( tr_handle * h )
|
|
|
|
{
|
|
|
|
tr_benc d;
|
|
|
|
const char * str;
|
2008-06-05 18:16:59 +00:00
|
|
|
char * username = tr_sessionGetRPCUsername( h );
|
|
|
|
char * password = tr_sessionGetRPCPassword( h );
|
|
|
|
char * auth = tr_strdup_printf( "%s:%s", username, password );
|
2008-05-18 16:44:30 +00:00
|
|
|
|
2008-06-05 18:16:59 +00:00
|
|
|
tr_bencInitDict( &d, 14 );
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_bencDictAddStr( &d, "download-dir", tr_sessionGetDownloadDir( h ) );
|
|
|
|
tr_bencDictAddInt( &d, "peer-limit", tr_sessionGetPeerLimit( h ) );
|
|
|
|
tr_bencDictAddInt( &d, "pex-allowed", tr_sessionIsPexEnabled( h ) );
|
2008-05-23 16:18:58 +00:00
|
|
|
tr_bencDictAddInt( &d, "port", tr_sessionGetPeerPort( h ) );
|
2008-06-05 18:16:59 +00:00
|
|
|
tr_bencDictAddStr( &d, "auth", auth );
|
|
|
|
tr_bencDictAddInt( &d, "auth-required",
|
|
|
|
tr_sessionIsRPCPasswordEnabled( h ) );
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_bencDictAddInt( &d, "port-forwarding-enabled",
|
|
|
|
tr_sessionIsPortForwardingEnabled( h ) );
|
|
|
|
tr_bencDictAddStr( &d, "rpc-acl", tr_sessionGetRPCACL( h ) );
|
|
|
|
tr_bencDictAddInt( &d, "rpc-port", tr_sessionGetRPCPort( h ) );
|
|
|
|
tr_bencDictAddInt( &d, "speed-limit-down",
|
|
|
|
tr_sessionGetSpeedLimit( h, TR_DOWN ) );
|
|
|
|
tr_bencDictAddInt( &d, "speed-limit-down-enabled",
|
|
|
|
tr_sessionIsSpeedLimitEnabled( h, TR_DOWN ) );
|
2008-05-22 19:24:11 +00:00
|
|
|
tr_bencDictAddInt( &d, "speed-limit-up",
|
|
|
|
tr_sessionGetSpeedLimit( h, TR_UP ) );
|
|
|
|
tr_bencDictAddInt( &d, "speed-limit-up-enabled",
|
|
|
|
tr_sessionIsSpeedLimitEnabled( h, TR_UP ) );
|
2008-05-18 16:44:30 +00:00
|
|
|
switch( tr_sessionGetEncryption( h ) ) {
|
|
|
|
case TR_PLAINTEXT_PREFERRED: str = "tolerated"; break;
|
|
|
|
case TR_ENCRYPTION_REQUIRED: str = "required"; break;
|
|
|
|
default: str = "preferred"; break;
|
2007-08-16 21:17:02 +00:00
|
|
|
}
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_bencDictAddStr( &d, "encryption", str );
|
2007-04-18 16:39:10 +00:00
|
|
|
|
2008-05-22 19:24:11 +00:00
|
|
|
tr_ninf( MY_NAME, "saving \"%s\"", myConfigFilename );
|
|
|
|
tr_bencSaveFile( myConfigFilename, &d );
|
2007-04-18 16:39:10 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_bencFree( &d );
|
2008-06-05 18:16:59 +00:00
|
|
|
tr_free( auth );
|
|
|
|
tr_free( password );
|
|
|
|
tr_free( username );
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
parseAuth( const char * auth, char ** username, char ** password )
|
|
|
|
{
|
|
|
|
int err = 0;
|
|
|
|
const char * pch = strchr( auth, ':' );
|
|
|
|
if( !pch )
|
|
|
|
err = -1;
|
|
|
|
else {
|
|
|
|
*username = tr_strndup( auth, pch-auth );
|
|
|
|
*password = tr_strdup( pch+1 );
|
|
|
|
}
|
|
|
|
return err;
|
2007-04-18 16:39:10 +00:00
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
static void
|
2008-06-05 18:16:59 +00:00
|
|
|
session_init( const char * configDir, int rpc_port,
|
|
|
|
const char * acl, const char * auth, int noauth )
|
2007-04-18 16:39:10 +00:00
|
|
|
{
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_benc state;
|
|
|
|
int have_state;
|
|
|
|
int64_t peer_port = TR_DEFAULT_PORT;
|
|
|
|
int64_t peers = TR_DEFAULT_GLOBAL_PEER_LIMIT;
|
|
|
|
int64_t pex_enabled = TR_DEFAULT_PEX_ENABLED;
|
|
|
|
int64_t fwd_enabled = TR_DEFAULT_PORT_FORWARDING_ENABLED;
|
|
|
|
int64_t up_limit = 100;
|
|
|
|
int64_t up_limited = FALSE;
|
|
|
|
int64_t down_limit = 100;
|
|
|
|
int64_t down_limited = FALSE;
|
|
|
|
int encryption = TR_ENCRYPTION_PREFERRED;
|
|
|
|
char downloadDir[MAX_PATH_LENGTH] = { '\0' };
|
2008-06-05 18:16:59 +00:00
|
|
|
const char * acl_fallback = TR_DEFAULT_RPC_ACL;
|
2008-05-18 16:44:30 +00:00
|
|
|
int64_t rpc_port_fallback = TR_DEFAULT_RPC_PORT;
|
2008-06-05 18:16:59 +00:00
|
|
|
int64_t auth_required_fallback = 0;
|
|
|
|
const char * auth_fallback = NULL;
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_ctor * ctor;
|
|
|
|
tr_torrent ** torrents;
|
2008-06-05 18:16:59 +00:00
|
|
|
int auth_required;
|
|
|
|
char * user = NULL;
|
|
|
|
char * pass = NULL;
|
2008-05-18 16:44:30 +00:00
|
|
|
|
2008-05-22 19:24:11 +00:00
|
|
|
if(( have_state = !tr_bencLoadFile( myConfigFilename, &state )))
|
2007-04-18 16:39:10 +00:00
|
|
|
{
|
2008-05-18 16:44:30 +00:00
|
|
|
const char * str;
|
2008-05-22 19:24:11 +00:00
|
|
|
tr_ninf( MY_NAME, "loading settings from \"%s\"", myConfigFilename );
|
2008-05-18 16:44:30 +00:00
|
|
|
|
|
|
|
if( tr_bencDictFindStr( &state, "download-dir", &str ) )
|
|
|
|
tr_strlcpy( downloadDir, str, sizeof( downloadDir ) );
|
|
|
|
tr_bencDictFindInt( &state, "peer-limit", &peers );
|
|
|
|
tr_bencDictFindInt( &state, "pex-allowed", &pex_enabled );
|
2008-05-22 19:24:11 +00:00
|
|
|
tr_bencDictFindInt( &state, "port", &peer_port );
|
|
|
|
tr_bencDictFindInt( &state, "port-forwarding-enabled", &fwd_enabled );
|
2008-06-05 18:16:59 +00:00
|
|
|
tr_bencDictFindStr( &state, "rpc-acl", &acl_fallback );
|
|
|
|
tr_bencDictFindStr( &state, "auth", &auth_fallback );
|
|
|
|
tr_bencDictFindInt( &state, "auth-required", &auth_required_fallback );
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_bencDictFindInt( &state, "rpc-port", &rpc_port_fallback );
|
|
|
|
tr_bencDictFindInt( &state, "speed-limit-down", &down_limit );
|
|
|
|
tr_bencDictFindInt( &state, "speed-limit-down-enabled", &down_limited );
|
|
|
|
tr_bencDictFindInt( &state, "speed-limit-up", &up_limit );
|
|
|
|
tr_bencDictFindInt( &state, "speed-limit-up-enabled", &up_limited );
|
|
|
|
if( tr_bencDictFindStr( &state, "encryption", &str ) ) {
|
|
|
|
if( !strcmp( str, "required" ) )
|
|
|
|
encryption = TR_ENCRYPTION_REQUIRED;
|
|
|
|
else if( !strcmp( str, "tolerated" ) )
|
|
|
|
encryption = TR_PLAINTEXT_PREFERRED;
|
|
|
|
}
|
2007-04-18 16:39:10 +00:00
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
/* fallbacks */
|
|
|
|
if( !*downloadDir )
|
|
|
|
getcwd( downloadDir, sizeof( downloadDir ) );
|
|
|
|
if( rpc_port < 1 )
|
|
|
|
rpc_port = rpc_port_fallback;
|
2008-06-05 18:16:59 +00:00
|
|
|
if( !acl || !*acl )
|
|
|
|
acl = acl_fallback;
|
|
|
|
if( !auth || !*auth )
|
|
|
|
auth = auth_fallback;
|
|
|
|
|
|
|
|
if( auth && parseAuth( auth, &user, &pass ) ) {
|
|
|
|
tr_nerr( MY_NAME, "Unable to parse authentication string \"%s\"", auth );
|
|
|
|
abort( );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( noauth ) {
|
|
|
|
/* user has explicitly turned off authentication */
|
|
|
|
user = NULL;
|
|
|
|
pass = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
auth_required = user || pass;
|
2008-05-18 16:44:30 +00:00
|
|
|
|
|
|
|
/* start the session */
|
2008-05-22 19:24:11 +00:00
|
|
|
mySession = tr_sessionInitFull( configDir, "daemon", downloadDir,
|
|
|
|
pex_enabled, fwd_enabled, peer_port,
|
|
|
|
encryption,
|
|
|
|
up_limit, up_limited,
|
|
|
|
down_limit, down_limited,
|
|
|
|
peers,
|
|
|
|
TR_MSG_INF, 0,
|
|
|
|
FALSE, /* is the blocklist enabled? */
|
|
|
|
TR_DEFAULT_PEER_SOCKET_TOS,
|
2008-06-05 18:16:59 +00:00
|
|
|
TRUE, rpc_port, acl,
|
|
|
|
auth_required, user, pass );
|
|
|
|
|
|
|
|
if( auth_required )
|
|
|
|
tr_ninf( MY_NAME, "requiring authentication" );
|
2008-05-18 16:44:30 +00:00
|
|
|
|
|
|
|
/* load the torrents */
|
2008-05-22 19:24:11 +00:00
|
|
|
ctor = tr_ctorNew( mySession );
|
|
|
|
torrents = tr_sessionLoadTorrents( mySession, ctor, NULL );
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_free( torrents );
|
|
|
|
tr_ctorFree( ctor );
|
|
|
|
|
|
|
|
if( have_state )
|
|
|
|
tr_bencFree( &state );
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
daemonUsage( void )
|
|
|
|
{
|
|
|
|
puts( "usage: " MY_NAME " [-dfh] [-p file] [-s file]\n"
|
|
|
|
"\n"
|
|
|
|
"Transmission " LONG_VERSION_STRING " http://www.transmissionbt.com/\n"
|
|
|
|
"A fast and easy BitTorrent client\n"
|
|
|
|
"\n"
|
|
|
|
" -a --acl <list> Access Control List. (Default: "TR_DEFAULT_RPC_ACL")\n"
|
|
|
|
" -f --foreground Run in the foreground and log to stderr\n"
|
|
|
|
" -g --config-dir <dir> Where to look for torrents and daemon-config.benc\n"
|
|
|
|
" -h --help Display this message and exit\n"
|
2008-06-05 18:16:59 +00:00
|
|
|
" -t --auth <user>:<pass> Username and password for authentication\n"
|
2008-05-19 16:16:38 +00:00
|
|
|
" -p --port n Port to listen to for requests (Default: "TR_DEFAULT_RPC_PORT_STR")\n"
|
2008-05-18 16:44:30 +00:00
|
|
|
"\n"
|
|
|
|
MY_NAME" is a headless Transmission session\n"
|
|
|
|
"that can be controlled via transmission-remote or Clutch.\n" );
|
2007-04-18 16:39:10 +00:00
|
|
|
exit( 0 );
|
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
static void
|
|
|
|
readargs( int argc, char ** argv,
|
2008-06-05 18:16:59 +00:00
|
|
|
int * nofork, int * port,
|
|
|
|
char ** configDir,
|
|
|
|
char ** acl,
|
|
|
|
char ** auth,
|
|
|
|
int * noauth )
|
2007-04-18 16:39:10 +00:00
|
|
|
{
|
2007-04-26 07:03:36 +00:00
|
|
|
int opt;
|
2008-06-05 18:16:59 +00:00
|
|
|
char optstr[] = "a:fg:hnp:t:u:w:";
|
2008-05-18 16:44:30 +00:00
|
|
|
struct option longopts[] = {
|
|
|
|
{ "acl", required_argument, NULL, 'a' },
|
|
|
|
{ "foreground", no_argument, NULL, 'f' },
|
|
|
|
{ "config-dir", required_argument, NULL, 'g' },
|
|
|
|
{ "help", no_argument, NULL, 'h' },
|
2008-06-05 18:16:59 +00:00
|
|
|
{ "noauth", no_argument, NULL, 'n' },
|
2008-05-18 16:44:30 +00:00
|
|
|
{ "port", required_argument, NULL, 'p' },
|
2008-06-05 18:16:59 +00:00
|
|
|
{ "auth", required_argument, NULL, 't' },
|
2008-05-18 16:44:30 +00:00
|
|
|
{ NULL, 0, NULL, '\0' }
|
|
|
|
};
|
|
|
|
while((( opt = getopt_long( argc, argv, optstr, longopts, NULL ))) != -1 ) {
|
2008-04-19 00:41:32 +00:00
|
|
|
switch( opt ) {
|
2008-05-18 16:44:30 +00:00
|
|
|
case 'a': *acl = tr_strdup( optarg ); break;
|
2008-04-19 00:41:32 +00:00
|
|
|
case 'f': *nofork = 1; break;
|
2008-06-05 18:16:59 +00:00
|
|
|
case 'n': *noauth = 1; break;
|
2008-05-18 16:44:30 +00:00
|
|
|
case 'g': *configDir = tr_strdup( optarg ); break;
|
2008-06-05 18:16:59 +00:00
|
|
|
case 't': *auth = tr_strdup( optarg ); break;
|
2008-05-18 16:44:30 +00:00
|
|
|
case 'p': *port = atoi( optarg ); break;
|
|
|
|
default: daemonUsage( ); break;
|
2007-04-18 16:39:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
static void
|
|
|
|
gotsig( int sig UNUSED )
|
2008-02-28 19:06:23 +00:00
|
|
|
{
|
2008-05-18 16:44:30 +00:00
|
|
|
closing = TRUE;
|
|
|
|
}
|
2008-02-28 19:06:23 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
#if !defined(HAVE_DAEMON)
|
|
|
|
static int
|
|
|
|
daemon( int nochdir, int noclose )
|
|
|
|
{
|
|
|
|
switch( fork( ) ) {
|
|
|
|
case 0:
|
2008-02-28 19:06:23 +00:00
|
|
|
break;
|
2008-05-18 16:44:30 +00:00
|
|
|
case -1:
|
2008-05-20 14:01:15 +00:00
|
|
|
tr_nerr( MY_NAME, "Error daemonizing (fork)! %d - %s", errno, strerror(errno) );
|
2008-05-18 16:44:30 +00:00
|
|
|
return -1;
|
2008-02-28 19:06:23 +00:00
|
|
|
default:
|
2008-05-18 16:44:30 +00:00
|
|
|
_exit(0);
|
2008-02-28 19:06:23 +00:00
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
if( setsid() < 0 ) {
|
2008-05-20 14:01:15 +00:00
|
|
|
tr_nerr( MY_NAME, "Error daemonizing (setsid)! %d - %s", errno, strerror(errno) );
|
2007-04-18 16:39:10 +00:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
switch( fork( ) ) {
|
|
|
|
case 0:
|
|
|
|
break;
|
|
|
|
case -1:
|
2008-05-20 14:01:15 +00:00
|
|
|
tr_nerr( MY_NAME, "Error daemonizing (fork2)! %d - %s", errno, strerror(errno) );
|
2008-05-18 16:44:30 +00:00
|
|
|
return -1;
|
|
|
|
default:
|
|
|
|
_exit(0);
|
2007-04-18 16:39:10 +00:00
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
if( !nochdir && 0 > chdir( "/" ) ) {
|
2008-05-20 14:01:15 +00:00
|
|
|
tr_nerr( MY_NAME, "Error daemonizing (chdir)! %d - %s", errno, strerror(errno) );
|
2007-04-18 16:39:10 +00:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
if( !noclose ) {
|
|
|
|
int fd;
|
|
|
|
if((( fd = open("/dev/null", O_RDONLY))) != 0 ) {
|
|
|
|
dup2( fd, 0 );
|
|
|
|
close( fd );
|
|
|
|
}
|
|
|
|
if((( fd = open("/dev/null", O_WRONLY))) != 1 ) {
|
|
|
|
dup2( fd, 1 );
|
|
|
|
close( fd );
|
|
|
|
}
|
|
|
|
if((( fd = open("/dev/null", O_WRONLY))) != 2 ) {
|
|
|
|
dup2( fd, 2 );
|
2007-04-18 16:39:10 +00:00
|
|
|
close( fd );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
return 0;
|
2007-04-18 16:39:10 +00:00
|
|
|
}
|
2008-05-18 16:44:30 +00:00
|
|
|
#endif
|
2007-04-18 16:39:10 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
int
|
|
|
|
main( int argc, char ** argv )
|
2007-04-18 16:39:10 +00:00
|
|
|
{
|
2008-05-18 16:44:30 +00:00
|
|
|
int nofork = 0;
|
2008-06-05 18:16:59 +00:00
|
|
|
int noauth = 0;
|
2008-05-18 16:44:30 +00:00
|
|
|
int port = TR_DEFAULT_RPC_PORT;
|
|
|
|
char * configDir = NULL;
|
|
|
|
char * acl = NULL;
|
2008-06-05 18:16:59 +00:00
|
|
|
char * auth = NULL;
|
2008-05-18 16:44:30 +00:00
|
|
|
|
|
|
|
signal( SIGINT, gotsig );
|
|
|
|
signal( SIGQUIT, gotsig );
|
|
|
|
signal( SIGTERM, gotsig );
|
2007-04-18 16:39:10 +00:00
|
|
|
signal( SIGPIPE, SIG_IGN );
|
|
|
|
signal( SIGHUP, SIG_IGN );
|
|
|
|
|
2008-06-05 18:16:59 +00:00
|
|
|
readargs( argc, argv, &nofork, &port, &configDir, &acl, &auth, &noauth );
|
2008-05-18 16:44:30 +00:00
|
|
|
if( configDir == NULL )
|
|
|
|
configDir = tr_strdup_printf( "%s-daemon", tr_getDefaultConfigDir() );
|
2008-05-22 19:24:11 +00:00
|
|
|
tr_buildPath( myConfigFilename, sizeof( myConfigFilename ),
|
2008-05-18 16:44:30 +00:00
|
|
|
configDir, "daemon-config.benc", NULL );
|
2007-04-18 16:39:10 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
if( !nofork ) {
|
|
|
|
if( 0 > daemon( 1, 0 ) ) {
|
|
|
|
fprintf( stderr, "failed to daemonize: %s\n", strerror( errno ) );
|
|
|
|
exit( 1 );
|
|
|
|
}
|
2007-04-18 16:39:10 +00:00
|
|
|
}
|
2007-08-14 04:02:50 +00:00
|
|
|
|
2008-06-05 18:16:59 +00:00
|
|
|
session_init( configDir, port, acl, auth, noauth );
|
2007-08-14 04:02:50 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
while( !closing )
|
|
|
|
sleep( 1 );
|
2007-08-14 04:02:50 +00:00
|
|
|
|
2008-05-22 19:24:11 +00:00
|
|
|
saveState( mySession );
|
2008-05-18 16:44:30 +00:00
|
|
|
printf( "Closing transmission session..." );
|
2008-05-22 19:24:11 +00:00
|
|
|
tr_sessionClose( mySession );
|
2008-05-18 16:44:30 +00:00
|
|
|
printf( " done.\n" );
|
2007-08-14 04:02:50 +00:00
|
|
|
|
2008-05-18 16:44:30 +00:00
|
|
|
tr_free( configDir );
|
2007-08-14 04:02:50 +00:00
|
|
|
return 0;
|
|
|
|
}
|