author | zautrix <zautrix> | 2004-07-03 17:03:05 (UTC) |
---|---|---|
committer | zautrix <zautrix> | 2004-07-03 17:03:05 (UTC) |
commit | d6c945f1c48c6b6742751177d07e88dca3f3044d (patch) (side-by-side diff) | |
tree | 8720a31b685480503aa3b5a3b3328dcddcdbc6cb /microkde/oprocess.cpp | |
parent | 65ed57d77ba421169b62f46f59ae6b1d7a228e3d (diff) | |
download | kdepimpi-d6c945f1c48c6b6742751177d07e88dca3f3044d.zip kdepimpi-d6c945f1c48c6b6742751177d07e88dca3f3044d.tar.gz kdepimpi-d6c945f1c48c6b6742751177d07e88dca3f3044d.tar.bz2 |
Added 4 opie files. Updated pro file.
-rw-r--r-- | microkde/oprocess.cpp | 951 |
1 files changed, 951 insertions, 0 deletions
diff --git a/microkde/oprocess.cpp b/microkde/oprocess.cpp new file mode 100644 index 0000000..95e3e4b --- a/dev/null +++ b/microkde/oprocess.cpp @@ -0,0 +1,951 @@ +/* + This file is part of the Opie Project + Copyright (C) 2002-2004 Holger Freyther <zecke@handhelds.org> + and The Opie Team <opie-devel@handhelds.org> + =. Based on KProcess (C) 1997 Christian Czezatke (e9025461@student.tuwien.ac.at) + .=l. + .>+-= +_;:, .> :=|. This program is free software; you can +.> <`_, > . <= redistribute it and/or modify it under +:`=1 )Y*s>-.-- : the terms of the GNU Library General Public +.="- .-=="i, .._ License as published by the Free Software +- . .-<_> .<> Foundation; either version 2 of the License, + ._= =} : or (at your option) any later version. + .%`+i> _;_. + .i_,=:_. -<s. This program is distributed in the hope that + + . -:. = it will be useful, but WITHOUT ANY WARRANTY; + : .. .:, . . . without even the implied warranty of + =_ + =;=|` MERCHANTABILITY or FITNESS FOR A + _.=:. : :=>`: PARTICULAR PURPOSE. See the GNU +..}^=.= = ; Library General Public License for more +++= -. .` .: details. +: = ...= . :.=- +-. .:....=;==+<; You should have received a copy of the GNU + -_. . . )=. = Library General Public License along with + -- :-=` this library; see the file COPYING.LIB. + If not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ + +#include "oprocctrl.h" + +/* OPIE */ +#include <oprocess.h> + +/* QT */ + +#include <qapplication.h> +#include <qdir.h> +#include <qmap.h> +#include <qsocketnotifier.h> +#include <qtextstream.h> + +/* STD */ +#include <errno.h> +#include <fcntl.h> +#include <pwd.h> +#include <stdlib.h> +#include <signal.h> +#include <stdio.h> +#include <string.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <unistd.h> +#ifdef HAVE_SYS_SELECT_H +#include <sys/select.h> +#endif +#ifdef HAVE_INITGROUPS +#include <grp.h> +#endif + +using namespace Opie::Core::Internal; + +namespace Opie { +namespace Core { +namespace Internal { +class OProcessPrivate +{ +public: + OProcessPrivate() : useShell( false ) + { } + + bool useShell; + QMap<QString, QString> env; + QString wd; + QCString shell; +}; +} + +OProcess::OProcess( QObject *parent, const char *name ) + : QObject( parent, name ) +{ + init ( ); +} + +OProcess::OProcess( const QString &arg0, QObject *parent, const char *name ) + : QObject( parent, name ) +{ + init ( ); + *this << arg0; +} + +OProcess::OProcess( const QStringList &args, QObject *parent, const char *name ) + : QObject( parent, name ) +{ + init ( ); + *this << args; +} + +void OProcess::init ( ) +{ + run_mode = NotifyOnExit; + runs = false; + pid_ = 0; + status = 0; + keepPrivs = false; + innot = 0; + outnot = 0; + errnot = 0; + communication = NoCommunication; + input_data = 0; + input_sent = 0; + input_total = 0; + d = 0; + + if ( 0 == OProcessController::theOProcessController ) + { + ( void ) new OProcessController(); + CHECK_PTR( OProcessController::theOProcessController ); + } + + OProcessController::theOProcessController->addOProcess( this ); + out[ 0 ] = out[ 1 ] = -1; + in[ 0 ] = in[ 1 ] = -1; + err[ 0 ] = err[ 1 ] = -1; +} + +void OProcess::setEnvironment( const QString &name, const QString &value ) +{ + if ( !d ) + d = new OProcessPrivate; + d->env.insert( name, value ); +} + +void OProcess::setWorkingDirectory( const QString &dir ) +{ + if ( !d ) + d = new OProcessPrivate; + d->wd = dir; +} + +void OProcess::setupEnvironment() +{ + if ( d ) + { + QMap<QString, QString>::Iterator it; + for ( it = d->env.begin(); it != d->env.end(); ++it ) + setenv( QFile::encodeName( it.key() ).data(), + QFile::encodeName( it.data() ).data(), 1 ); + if ( !d->wd.isEmpty() ) + chdir( QFile::encodeName( d->wd ).data() ); + } +} + +void OProcess::setRunPrivileged( bool keepPrivileges ) +{ + keepPrivs = keepPrivileges; +} + +bool OProcess::runPrivileged() const +{ + return keepPrivs; +} + +OProcess::~OProcess() +{ + // destroying the OProcess instance sends a SIGKILL to the + // child process (if it is running) after removing it from the + // list of valid processes (if the process is not started as + // "DontCare") + + OProcessController::theOProcessController->removeOProcess( this ); + // this must happen before we kill the child + // TODO: block the signal while removing the current process from the process list + + if ( runs && ( run_mode != DontCare ) ) + kill( SIGKILL ); + + // Clean up open fd's and socket notifiers. + closeStdin(); + closeStdout(); + closeStderr(); + + // TODO: restore SIGCHLD and SIGPIPE handler if this is the last OProcess + delete d; +} + +void OProcess::detach() +{ + OProcessController::theOProcessController->removeOProcess( this ); + + runs = false; + pid_ = 0; + + // Clean up open fd's and socket notifiers. + closeStdin(); + closeStdout(); + closeStderr(); +} + +bool OProcess::setExecutable( const QString& proc ) +{ + if ( runs ) + return false; + + if ( proc.isEmpty() ) + return false; + + if ( !arguments.isEmpty() ) + arguments.remove( arguments.begin() ); + arguments.prepend( QFile::encodeName( proc ) ); + + return true; +} + +OProcess &OProcess::operator<<( const QStringList& args ) +{ + QStringList::ConstIterator it = args.begin(); + for ( ; it != args.end() ; ++it ) + arguments.append( QFile::encodeName( *it ) ); + return *this; +} + +OProcess &OProcess::operator<<( const QCString& arg ) +{ + return operator<< ( arg.data() ); +} + +OProcess &OProcess::operator<<( const char* arg ) +{ + arguments.append( arg ); + return *this; +} + +OProcess &OProcess::operator<<( const QString& arg ) +{ + arguments.append( QFile::encodeName( arg ) ); + return *this; +} + +void OProcess::clearArguments() +{ + arguments.clear(); +} + +bool OProcess::start( RunMode runmode, Communication comm ) +{ + uint i; + uint n = arguments.count(); + char **arglist; + + if ( runs || ( 0 == n ) ) + { + return false; // cannot start a process that is already running + // or if no executable has been assigned + } + run_mode = runmode; + status = 0; + + QCString shellCmd; + if ( d && d->useShell ) + { + if ( d->shell.isEmpty() ) + { + qWarning( "Could not find a valid shell" ); + return false; + } + + arglist = static_cast<char **>( malloc( ( 4 ) * sizeof( char * ) ) ); + for ( i = 0; i < n; i++ ) + { + shellCmd += arguments[ i ]; + shellCmd += " "; // CC: to separate the arguments + } + + arglist[ 0 ] = d->shell.data(); + arglist[ 1 ] = ( char * ) "-c"; + arglist[ 2 ] = shellCmd.data(); + arglist[ 3 ] = 0; + } + else + { + arglist = static_cast<char **>( malloc( ( n + 1 ) * sizeof( char * ) ) ); + for ( i = 0; i < n; i++ ) + arglist[ i ] = arguments[ i ].data(); + arglist[ n ] = 0; + } + + if ( !setupCommunication( comm ) ) + qWarning( "Could not setup Communication!" ); + + // We do this in the parent because if we do it in the child process + // gdb gets confused when the application runs from gdb. + uid_t uid = getuid(); + gid_t gid = getgid(); +#ifdef HAVE_INITGROUPS + + struct passwd *pw = getpwuid( uid ); +#endif + + int fd[ 2 ]; + if ( 0 > pipe( fd ) ) + { + fd[ 0 ] = fd[ 1 ] = 0; // Pipe failed.. continue + } + + runs = true; + + QApplication::flushX(); + + // WABA: Note that we use fork() and not vfork() because + // vfork() has unclear semantics and is not standardized. + pid_ = fork(); + + if ( 0 == pid_ ) + { + if ( fd[ 0 ] ) + close( fd[ 0 ] ); + if ( !runPrivileged() ) + { + setgid( gid ); +#if defined( HAVE_INITGROUPS) + + if ( pw ) + initgroups( pw->pw_name, pw->pw_gid ); +#endif + + setuid( uid ); + } + // The child process + if ( !commSetupDoneC() ) + qWarning( "Could not finish comm setup in child!" ); + + setupEnvironment(); + + // Matthias + if ( run_mode == DontCare ) + setpgid( 0, 0 ); + // restore default SIGPIPE handler (Harri) + struct sigaction act; + sigemptyset( &( act.sa_mask ) ); + sigaddset( &( act.sa_mask ), SIGPIPE ); + act.sa_handler = SIG_DFL; + act.sa_flags = 0; + sigaction( SIGPIPE, &act, 0L ); + + // We set the close on exec flag. + // Closing of fd[1] indicates that the execvp succeeded! + if ( fd[ 1 ] ) + fcntl( fd[ 1 ], F_SETFD, FD_CLOEXEC ); + execvp( arglist[ 0 ], arglist ); + char resultByte = 1; + if ( fd[ 1 ] ) + write( fd[ 1 ], &resultByte, 1 ); + _exit( -1 ); + } + else if ( -1 == pid_ ) + { + // forking failed + + runs = false; + free( arglist ); + return false; + } + else + { + if ( fd[ 1 ] ) + close( fd[ 1 ] ); + // the parent continues here + + // Discard any data for stdin that might still be there + input_data = 0; + + // Check whether client could be started. + if ( fd[ 0 ] ) + for ( ;; ) + { + char resultByte; + int n = ::read( fd[ 0 ], &resultByte, 1 ); + if ( n == 1 ) + { + // Error + runs = false; + close( fd[ 0 ] ); + free( arglist ); + pid_ = 0; + return false; + } + if ( n == -1 ) + { + if ( ( errno == ECHILD ) || ( errno == EINTR ) ) + continue; // Ignore + } + break; // success + } + if ( fd[ 0 ] ) + close( fd[ 0 ] ); + + if ( !commSetupDoneP() ) // finish communication socket setup for the parent + qWarning( "Could not finish comm setup in parent!" ); + + if ( run_mode == Block ) + { + commClose(); + + // The SIGCHLD handler of the process controller will catch + // the exit and set the status + while ( runs ) + { + OProcessController::theOProcessController-> + slotDoHousekeeping( 0 ); + } + runs = FALSE; + emit processExited( this ); + } + } + free( arglist ); + return true; +} + + + +bool OProcess::kill( int signo ) +{ + bool rv = false; + + if ( 0 != pid_ ) + rv = ( -1 != ::kill( pid_, signo ) ); + // probably store errno somewhere... + return rv; +} + +bool OProcess::isRunning() const +{ + return runs; +} + +pid_t OProcess::pid() const +{ + return pid_; +} + +bool OProcess::normalExit() const +{ + int _status = status; + return ( pid_ != 0 ) && ( !runs ) && ( WIFEXITED( ( _status ) ) ); +} + +int OProcess::exitStatus() const +{ + int _status = status; + return WEXITSTATUS( ( _status ) ); +} + +bool OProcess::writeStdin( const char *buffer, int buflen ) +{ + bool rv; + + // if there is still data pending, writing new data + // to stdout is not allowed (since it could also confuse + // kprocess... + if ( 0 != input_data ) + return false; + + if ( runs && ( communication & Stdin ) ) + { + input_data = buffer; + input_sent = 0; + input_total = buflen; + slotSendData( 0 ); + innot->setEnabled( true ); + rv = true; + } + else + rv = false; + return rv; +} + +void OProcess::flushStdin ( ) +{ + if ( !input_data || ( input_sent == input_total ) ) + return ; + + int d1, d2; + + do + { + d1 = input_total - input_sent; + slotSendData ( 0 ); + d2 = input_total - input_sent; + } + while ( d2 <= d1 ); +} + +void OProcess::suspend() +{ + if ( ( communication & Stdout ) && outnot ) + outnot->setEnabled( false ); +} + +void OProcess::resume() +{ + if ( ( communication & Stdout ) && outnot ) + outnot->setEnabled( true ); +} + +bool OProcess::closeStdin() +{ + bool rv; + + if ( communication & Stdin ) + { + communication = ( Communication ) ( communication & ~Stdin ); + delete innot; + innot = 0; + close( in[ 1 ] ); + rv = true; + } + else + rv = false; + return rv; +} + +bool OProcess::closeStdout() +{ + bool rv; + + if ( communication & Stdout ) + { + communication = ( Communication ) ( communication & ~Stdout ); + delete outnot; + outnot = 0; + close( out[ 0 ] ); + rv = true; + } + else + rv = false; + return rv; +} + +bool OProcess::closeStderr() +{ + bool rv; + + if ( communication & Stderr ) + { + communication = static_cast<Communication>( communication & ~Stderr ); + delete errnot; + errnot = 0; + close( err[ 0 ] ); + rv = true; + } + else + rv = false; + return rv; +} + +void OProcess::slotChildOutput( int fdno ) +{ + if ( !childOutput( fdno ) ) + closeStdout(); +} + +void OProcess::slotChildError( int fdno ) +{ + if ( !childError( fdno ) ) + closeStderr(); +} + +void OProcess::slotSendData( int ) +{ + if ( input_sent == input_total ) + { + innot->setEnabled( false ); + input_data = 0; + emit wroteStdin( this ); + } + else + input_sent += ::write( in[ 1 ], input_data + input_sent, input_total - input_sent ); +} + +void OProcess::processHasExited( int state ) +{ + if ( runs ) + { + runs = false; + status = state; + + commClose(); // cleanup communication sockets + + // also emit a signal if the process was run Blocking + if ( DontCare != run_mode ) + { + emit processExited( this ); + } + } +} + +int OProcess::childOutput( int fdno ) +{ + if ( communication & NoRead ) + { + int len = -1; + emit receivedStdout( fdno, len ); + errno = 0; // Make sure errno doesn't read "EAGAIN" + return len; + } + else + { + char buffer[ 1024 ]; + int len; + + len = ::read( fdno, buffer, 1024 ); + + if ( 0 < len ) + { + emit receivedStdout( this, buffer, len ); + } + return len; + } +} + +int OProcess::childError( int fdno ) +{ + char buffer[ 1024 ]; + int len; + + len = ::read( fdno, buffer, 1024 ); + + if ( 0 < len ) + emit receivedStderr( this, buffer, len ); + return len; +} + +int OProcess::setupCommunication( Communication comm ) +{ + int ok; + + communication = comm; + + ok = 1; + if ( comm & Stdin ) + ok &= socketpair( AF_UNIX, SOCK_STREAM, 0, in ) >= 0; + + if ( comm & Stdout ) + ok &= socketpair( AF_UNIX, SOCK_STREAM, 0, out ) >= 0; + + if ( comm & Stderr ) + ok &= socketpair( AF_UNIX, SOCK_STREAM, 0, err ) >= 0; + + return ok; +} + +int OProcess::commSetupDoneP() +{ + int ok = 1; + + if ( communication != NoCommunication ) + { + if ( communication & Stdin ) + close( in[ 0 ] ); + if ( communication & Stdout ) + close( out[ 1 ] ); + if ( communication & Stderr ) + close( err[ 1 ] ); + + // Don't create socket notifiers and set the sockets non-blocking if + // blocking is requested. + if ( run_mode == Block ) + return ok; + + if ( communication & Stdin ) + { + // ok &= (-1 != fcntl(in[1], F_SETFL, O_NONBLOCK)); + innot = new QSocketNotifier( in[ 1 ], QSocketNotifier::Write, this ); + CHECK_PTR( innot ); + innot->setEnabled( false ); // will be enabled when data has to be sent + QObject::connect( innot, SIGNAL( activated(int) ), + this, SLOT( slotSendData(int) ) ); + } + + if ( communication & Stdout ) + { + // ok &= (-1 != fcntl(out[0], F_SETFL, O_NONBLOCK)); + outnot = new QSocketNotifier( out[ 0 ], QSocketNotifier::Read, this ); + CHECK_PTR( outnot ); + QObject::connect( outnot, SIGNAL( activated(int) ), + this, SLOT( slotChildOutput(int) ) ); + if ( communication & NoRead ) + suspend(); + } + + if ( communication & Stderr ) + { + // ok &= (-1 != fcntl(err[0], F_SETFL, O_NONBLOCK)); + errnot = new QSocketNotifier( err[ 0 ], QSocketNotifier::Read, this ); + CHECK_PTR( errnot ); + QObject::connect( errnot, SIGNAL( activated(int) ), + this, SLOT( slotChildError(int) ) ); + } + } + return ok; +} + +int OProcess::commSetupDoneC() +{ + int ok = 1; + struct linger so; + memset( &so, 0, sizeof( so ) ); + + if ( communication & Stdin ) + close( in[ 1 ] ); + if ( communication & Stdout ) + close( out[ 0 ] ); + if ( communication & Stderr ) + close( err[ 0 ] ); + + if ( communication & Stdin ) + ok &= dup2( in[ 0 ], STDIN_FILENO ) != -1; + else + { + int null_fd = open( "/dev/null", O_RDONLY ); + ok &= dup2( null_fd, STDIN_FILENO ) != -1; + close( null_fd ); + } + if ( communication & Stdout ) + { + ok &= dup2( out[ 1 ], STDOUT_FILENO ) != -1; + ok &= !setsockopt( out[ 1 ], SOL_SOCKET, SO_LINGER, ( char* ) & so, sizeof( so ) ); + } + else + { + int null_fd = open( "/dev/null", O_WRONLY ); + ok &= dup2( null_fd, STDOUT_FILENO ) != -1; + close( null_fd ); + } + if ( communication & Stderr ) + { + ok &= dup2( err[ 1 ], STDERR_FILENO ) != -1; + ok &= !setsockopt( err[ 1 ], SOL_SOCKET, SO_LINGER, reinterpret_cast<char *>( &so ), sizeof( so ) ); + } + else + { + int null_fd = open( "/dev/null", O_WRONLY ); + ok &= dup2( null_fd, STDERR_FILENO ) != -1; + close( null_fd ); + } + return ok; +} + +void OProcess::commClose() +{ + if ( NoCommunication != communication ) + { + bool b_in = ( communication & Stdin ); + bool b_out = ( communication & Stdout ); + bool b_err = ( communication & Stderr ); + if ( b_in ) + delete innot; + + if ( b_out || b_err ) + { + // If both channels are being read we need to make sure that one socket buffer + // doesn't fill up whilst we are waiting for data on the other (causing a deadlock). + // Hence we need to use select. + + // Once one or other of the channels has reached EOF (or given an error) go back + // to the usual mechanism. + + int fds_ready = 1; + fd_set rfds; + + int max_fd = 0; + if ( b_out ) + { + fcntl( out[ 0 ], F_SETFL, O_NONBLOCK ); + if ( out[ 0 ] > max_fd ) + max_fd = out[ 0 ]; + delete outnot; + outnot = 0; + } + if ( b_err ) + { + fcntl( err[ 0 ], F_SETFL, O_NONBLOCK ); + if ( err[ 0 ] > max_fd ) + max_fd = err[ 0 ]; + delete errnot; + errnot = 0; + } + + + while ( b_out || b_err ) + { + // * If the process is still running we block until we + // receive data. (p_timeout = 0, no timeout) + // * If the process has already exited, we only check + // the available data, we don't wait for more. + // (p_timeout = &timeout, timeout immediately) + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 0; + struct timeval *p_timeout = runs ? 0 : &timeout; + + FD_ZERO( &rfds ); + if ( b_out ) + FD_SET( out[ 0 ], &rfds ); + + if ( b_err ) + FD_SET( err[ 0 ], &rfds ); + + fds_ready = select( max_fd + 1, &rfds, 0, 0, p_timeout ); + if ( fds_ready <= 0 ) + break; + + if ( b_out && FD_ISSET( out[ 0 ], &rfds ) ) + { + int ret = 1; + while ( ret > 0 ) + ret = childOutput( out[ 0 ] ); + if ( ( ret == -1 && errno != EAGAIN ) || ret == 0 ) + b_out = false; + } + + if ( b_err && FD_ISSET( err[ 0 ], &rfds ) ) + { + int ret = 1; + while ( ret > 0 ) + ret = childError( err[ 0 ] ); + if ( ( ret == -1 && errno != EAGAIN ) || ret == 0 ) + b_err = false; + } + } + } + + if ( b_in ) + { + communication = ( Communication ) ( communication & ~Stdin ); + close( in[ 1 ] ); + } + if ( b_out ) + { + communication = ( Communication ) ( communication & ~Stdout ); + close( out[ 0 ] ); + } + if ( b_err ) + { + communication = ( Communication ) ( communication & ~Stderr ); + close( err[ 0 ] ); + } + } +} + +void OProcess::setUseShell( bool useShell, const char *shell ) +{ + if ( !d ) + d = new OProcessPrivate; + d->useShell = useShell; + d->shell = shell; + if ( d->shell.isEmpty() ) + d->shell = searchShell(); +} + +QString OProcess::quote( const QString &arg ) +{ + QString res = arg; + res.replace( QRegExp( QString::fromLatin1( "\'" ) ), + QString::fromLatin1( "'\"'\"'" ) ); + res.prepend( '\'' ); + res.append( '\'' ); + return res; +} + +QCString OProcess::searchShell() +{ + QCString tmpShell = QCString( getenv( "SHELL" ) ).stripWhiteSpace(); + if ( !isExecutable( tmpShell ) ) + { + tmpShell = "/bin/sh"; + } + + return tmpShell; +} + +bool OProcess::isExecutable( const QCString &filename ) +{ + struct stat fileinfo; + + if ( filename.isEmpty() ) + return false; + + // CC: we've got a valid filename, now let's see whether we can execute that file + + if ( -1 == stat( filename.data(), &fileinfo ) ) + return false; + // CC: return false if the file does not exist + + // CC: anyway, we cannot execute directories, block/character devices, fifos or sockets + if ( ( S_ISDIR( fileinfo.st_mode ) ) || + ( S_ISCHR( fileinfo.st_mode ) ) || + ( S_ISBLK( fileinfo.st_mode ) ) || +#ifdef S_ISSOCK + // CC: SYSVR4 systems don't have that macro + ( S_ISSOCK( fileinfo.st_mode ) ) || +#endif + ( S_ISFIFO( fileinfo.st_mode ) ) || + ( S_ISDIR( fileinfo.st_mode ) ) ) + { + return false; + } + + // CC: now check for permission to execute the file + if ( access( filename.data(), X_OK ) != 0 ) + return false; + + // CC: we've passed all the tests... + return true; +} + +int OProcess::processPID( const QString& process ) +{ + QString line; + QDir d = QDir( "/proc" ); + QStringList dirs = d.entryList( QDir::Dirs ); + QStringList::Iterator it; + for ( it = dirs.begin(); it != dirs.end(); ++it ) + { + //qDebug( "next entry: %s", (const char*) *it ); + QFile file( "/proc/"+*it+"/cmdline" ); + file.open( IO_ReadOnly ); + if ( !file.isOpen() ) continue; + QTextStream t( &file ); + line = t.readLine(); + //qDebug( "cmdline = %s", (const char*) line ); + if ( line.contains( process ) ) break; //FIXME: That may find also other process, if the name is not long enough ;) + } + if ( line.contains( process ) ) + { + //qDebug( "found process id #%d", (*it).toInt() ); + return (*it).toInt(); + } + else + { + //qDebug( "process '%s' not found", (const char*) process ); + return 0; + } +} + +} +} |