tdelibs/dcop/client/dcop.cpp

926 lines
25 KiB

/*****************************************************************
Copyright (c) 2000 Matthias Ettrich <ettrich@kde.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************/
// putenv() is not available on all platforms, so make sure the emulation
// wrapper is available in those cases by loading config.h!
#include <config.h>
#include <sys/types.h>
#include <pwd.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <tqbuffer.h>
#include <tqcolor.h>
#include <tqdir.h>
#include <tqfile.h>
#include <tqfileinfo.h>
#include <tqimage.h>
#include <tqmap.h>
#include <tqstringlist.h>
#include <tqtextstream.h>
#include <tqvariant.h>
#include "../dcopclient.h"
#include "../dcopref.h"
#include "../kdatastream.h"
#include "marshall.cpp"
#if defined Q_WS_X11
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#endif
typedef TQMap<TQString, TQString> UserList;
static DCOPClient* dcop = 0;
static TQTextStream cin_ ( stdin, IO_ReadOnly );
static TQTextStream cout_( stdout, IO_WriteOnly );
static TQTextStream cerr_( stderr, IO_WriteOnly );
/**
* Session to send call to
* DefaultSession - current session. Current KDE session when called without
* --user or --all-users option. Otherwise this value ignores
* all users with more than one active session.
* AllSessions - Send to all sessions found. requires --user or --all-users.
* QuerySessions - Don't call DCOP, return a list of available sessions.
* CustomSession - Use the specified session
*/
enum Session { DefaultSession = 0, AllSessions, QuerySessions, CustomSession };
bool startsWith(const TQCString &id, const char *str, int n)
{
return !n || (strncmp(id.data(), str, n) == 0);
}
bool endsWith(TQCString &id, char c)
{
if (id.length() && (id[id.length()-1] == c))
{
id.truncate(id.length()-1);
return true;
}
return false;
}
void queryApplications(const TQCString &filter)
{
int filterLen = filter.length();
QCStringList apps = dcop->registeredApplications();
for ( QCStringList::Iterator it = apps.begin(); it != apps.end(); ++it )
{
TQCString &clientId = *it;
if ( (clientId != dcop->appId()) &&
!startsWith(clientId, "anonymous",9) &&
startsWith(clientId, filter, filterLen)
)
printf( "%s\n", clientId.data() );
}
if ( !dcop->isAttached() )
{
qWarning( "server not accessible" );
exit(1);
}
}
void queryObjects( const TQCString &app, const TQCString &filter )
{
int filterLen = filter.length();
bool ok = false;
bool isDefault = false;
QCStringList objs = dcop->remoteObjects( app, &ok );
for ( QCStringList::Iterator it = objs.begin(); it != objs.end(); ++it )
{
TQCString &objId = *it;
if (objId == "default")
{
isDefault = true;
continue;
}
if (startsWith(objId, filter, filterLen))
{
if (isDefault)
printf( "%s (default)\n", objId.data() );
else
printf( "%s\n", objId.data() );
}
isDefault = false;
}
if ( !ok )
{
if (!dcop->isApplicationRegistered(app))
qWarning( "No such application: '%s'", app.data());
else
qWarning( "Application '%s' not accessible", app.data() );
exit(1);
}
}
void queryFunctions( const char* app, const char* obj )
{
bool ok = false;
QCStringList funcs = dcop->remoteFunctions( app, obj, &ok );
for ( QCStringList::Iterator it = funcs.begin(); it != funcs.end(); ++it ) {
printf( "%s\n", (*it).data() );
}
if ( !ok )
{
qWarning( "object '%s' in application '%s' not accessible", obj, app );
exit( 1 );
}
}
int callFunction( const char* app, const char* obj, const char* func, const QCStringList args )
{
TQString f = func; // Qt is better with unicode strings, so use one.
int left = f.find( '(' );
int right = f.find( ')' );
if ( right < left )
{
qWarning( "parentheses do not match" );
return( 1 );
}
if ( left < 0 ) {
// try to get the interface from the server
bool ok = false;
QCStringList funcs = dcop->remoteFunctions( app, obj, &ok );
TQCString realfunc;
if ( !ok && args.isEmpty() )
goto doit;
if ( !ok )
{
qWarning( "object not accessible" );
return( 1 );
}
for ( QCStringList::Iterator it = funcs.begin(); it != funcs.end(); ++it ) {
int l = (*it).find( '(' );
int s;
if (l > 0)
s = (*it).findRev( ' ', l);
else
s = (*it).find( ' ' );
if ( s < 0 )
s = 0;
else
s++;
if ( l > 0 && (*it).mid( s, l - s ) == func ) {
realfunc = (*it).mid( s );
const TQString arguments = (*it).mid(l+1,(*it).find( ')' )-l-1);
uint a = arguments.contains(',');
if ( (a==0 && !arguments.isEmpty()) || a>0)
a++;
if ( a == args.count() )
break;
}
}
if ( realfunc.isEmpty() )
{
qWarning("no such function");
return( 1 );
}
f = realfunc;
left = f.find( '(' );
right = f.find( ')' );
}
doit:
if ( left < 0 )
f += "()";
// This may seem expensive but is done only once per invocation
// of dcop, so it should be OK.
//
//
TQStringList intTypes;
intTypes << "int" << "unsigned" << "long" << "bool" ;
TQStringList types;
if ( left >0 && left + 1 < right - 1) {
types = TQStringList::split( ',', f.mid( left + 1, right - left - 1) );
for ( TQStringList::Iterator it = types.begin(); it != types.end(); ++it ) {
TQString lt = (*it).simplifyWhiteSpace();
int s = lt.find(' ');
// If there are spaces in the name, there may be two
// reasons: the parameter name is still there, ie.
// "TQString URL" or it's a complicated int type, ie.
// "unsigned long long int bool".
//
//
if ( s > 0 )
{
TQStringList partl = TQStringList::split(' ' , lt);
// The zero'th part is -- at the very least -- a
// type part. Any trailing parts *might* be extra
// int-type keywords, or at most one may be the
// parameter name.
//
//
s=1;
while (s < static_cast<int>(partl.count()) && intTypes.contains(partl[s]))
{
s++;
}
if ( s < static_cast<int>(partl.count())-1)
{
qWarning("The argument `%s' seems syntactically wrong.",
lt.latin1());
}
if ( s == static_cast<int>(partl.count())-1)
{
partl.remove(partl.at(s));
}
lt = partl.join(" ");
lt = lt.simplifyWhiteSpace();
}
(*it) = lt;
}
TQString fc = f.left( left );
fc += '(';
bool first = true;
for ( TQStringList::Iterator it = types.begin(); it != types.end(); ++it ) {
if ( !first )
fc +=",";
first = false;
fc += *it;
}
fc += ')';
f = fc;
}
TQByteArray data, replyData;
TQCString replyType;
TQDataStream arg(data, IO_WriteOnly);
uint i = 0;
for( TQStringList::Iterator it = types.begin(); it != types.end(); ++it ) {
marshall( arg, args, i, *it );
}
if ( i != args.count() )
{
qWarning( "arguments do not match" );
return( 1 );
}
if ( !dcop->call( app, obj, f.latin1(), data, replyType, replyData) ) {
qWarning( "call failed");
return( 1 );
} else {
TQDataStream reply(replyData, IO_ReadOnly);
if ( replyType != "void" && replyType != "ASYNC" )
{
TQCString replyString = demarshal( reply, replyType );
if ( !replyString.isEmpty() )
printf( "%s\n", replyString.data() );
else
printf("\n");
}
}
return 0;
}
/**
* Show command-line help and exit
*/
void showHelp( int exitCode = 0 )
{
#ifdef DCOPQUIT
cout_ << "Usage: dcopquit [options] [application]" << endl
#else
cout_ << "Usage: dcop [options] [application [object [function [arg1] [arg2] ... ] ] ]" << endl
#endif
<< "" << endl
<< "Console DCOP client" << endl
<< "" << endl
<< "Generic options:" << endl
<< " --help Show help about options" << endl
<< "" << endl
<< "Options:" << endl
<< " --pipe Call DCOP for each line read from stdin. The string '%1'" << endl
<< " will be used in the argument list as a placeholder for" << endl
<< " the substituted line." << endl
<< " For example," << endl
<< " dcop --pipe konqueror html-widget1 evalJS %1" << endl
<< " is equivalent to calling" << endl
<< " while read line ; do" << endl
<< " dcop konqueror html-widget1 evalJS \"$line\"" << endl
<< " done" << endl
<< " in bash, but because no new dcop instance has to be started" << endl
<< " for each line this is generally much faster, especially for" << endl
<< " the slower GNU dynamic linkers." << endl
<< " The '%1' placeholder cannot be used to replace e.g. the" << endl
<< " program, object or method name." << endl
<< " --user <user> Connect to the given user's DCOP server. This option will" << endl
<< " ignore the values of the environment vars $DCOPSERVER and" << endl
<< " $ICEAUTHORITY, even if they are set." << endl
<< " If the user has more than one open session, you must also" << endl
<< " use one of the --list-sessions, --session or --all-sessions" << endl
<< " command-line options." << endl
<< " --all-users Send the same DCOP call to all users with a running DCOP" << endl
<< " server. Only failed calls to existing DCOP servers will" << endl
<< " generate an error message. If no DCOP server is available" << endl
<< " at all, no error will be generated." << endl
<< " --session <ses> Send to the given KDE session. This option can only be" << endl
<< " used in combination with the --user option." << endl
<< " --all-sessions Send to all sessions found. Only works with the --user" << endl
<< " and --all-users options." << endl
<< " --list-sessions List all active KDE session for a user or all users." << endl
<< " --no-user-time Don't update the user activity timestamp in the called" << endl
<< " application (for usage in scripts running" << endl
<< " in the background)." << endl
<< endl;
exit( exitCode );
}
/**
* Return a list of all users and their home directories.
* Returns an empty list if /etc/passwd cannot be read for some reason.
*/
static UserList userList()
{
UserList result;
while( passwd* pstruct = getpwent() )
{
result[ TQString::fromLocal8Bit(pstruct->pw_name) ] = TQFile::decodeName(pstruct->pw_dir);
}
return result;
}
/**
* Return a list of available DCOP sessions for the specified user
* An empty list means no sessions are available, or an error occurred.
*/
TQStringList dcopSessionList( const TQString &user, const TQString &home )
{
if( home.isEmpty() )
{
cerr_ << "WARNING: Cannot determine home directory for user "
<< user << "!" << endl
<< "Please check permissions or set the $DCOPSERVER variable manually before" << endl
<< "calling dcop." << endl;
return TQStringList();
}
TQStringList result;
TQFileInfo dirInfo( home );
if( !dirInfo.exists() || !dirInfo.isReadable() )
return result;
TQDir d( home );
d.setFilter( TQDir::Files | TQDir::Hidden | TQDir::NoSymLinks );
d.setNameFilter( ".DCOPserver*" );
const TQFileInfoList *list = d.entryInfoList();
if( !list )
return result;
TQFileInfoListIterator it( *list );
TQFileInfo *fi;
while ( ( fi = it.current() ) != 0 )
{
if( fi->isReadable() )
result.append( fi->fileName() );
++it;
}
return result;
}
void sendUserTime( const char* app )
{
#if defined Q_WS_X11
static unsigned long time = 0;
if( time == 0 )
{
Display* dpy = XOpenDisplay( NULL );
if( dpy != NULL )
{
Window w = XCreateSimpleWindow( dpy, DefaultRootWindow( dpy ), 0, 0, 1, 1, 0, 0, 0 );
XSelectInput( dpy, w, PropertyChangeMask );
unsigned char data[ 1 ];
XChangeProperty( dpy, w, XA_ATOM, XA_ATOM, 8, PropModeAppend, data, 1 );
XEvent ev;
XWindowEvent( dpy, w, PropertyChangeMask, &ev );
time = ev.xproperty.time;
XDestroyWindow( dpy, w );
}
}
DCOPRef( app, "MainApplication-Interface" ).call( "updateUserTimestamp", time );
#else
// ...
#endif
}
/**
* Do the actual DCOP call
*/
int runDCOP( QCStringList args, UserList users, Session session,
const TQString sessionName, bool readStdin, bool updateUserTime )
{
bool DCOPrefmode=false;
TQCString app;
TQCString objid;
TQCString function;
QCStringList params;
DCOPClient *client = 0L;
int retval = 0;
if ( !args.isEmpty() && args[ 0 ].find( "DCOPRef(" ) == 0 )
{
int delimPos = args[ 0 ].findRev( ',' );
if( delimPos == -1 )
{
cerr_ << "Error: '" << args[ 0 ]
<< "' is not a valid DCOP reference." << endl;
exit( -1 );
}
app = args[ 0 ].mid( 8, delimPos-8 );
delimPos++;
objid = args[ 0 ].mid( delimPos, args[ 0 ].length()-delimPos-1 );
if( args.count() > 1 )
function = args[ 1 ];
if( args.count() > 2 )
{
params = args;
params.remove( params.begin() );
params.remove( params.begin() );
}
DCOPrefmode=true;
}
else
{
if( !args.isEmpty() )
app = args[ 0 ];
if( args.count() > 1 )
objid = args[ 1 ];
if( args.count() > 2 )
function = args[ 2 ];
if( args.count() > 3)
{
params = args;
params.remove( params.begin() );
params.remove( params.begin() );
params.remove( params.begin() );
}
}
bool firstRun = true;
UserList::Iterator it;
TQStringList sessions;
bool presetDCOPServer = false;
// char *dcopStr = 0L;
TQString dcopServer;
for( it = users.begin(); it != users.end() || firstRun; ++it )
{
firstRun = false;
//cout_ << "Iterating '" << it.key() << "'" << endl;
if( session == QuerySessions )
{
TQStringList sessions = dcopSessionList( it.key(), it.data() );
if( sessions.isEmpty() )
{
if( users.count() <= 1 )
{
cout_ << "No active sessions";
if( !( *it ).isEmpty() )
cout_ << " for user " << *it;
cout_ << endl;
}
}
else
{
cout_ << "Active sessions ";
if( !( *it ).isEmpty() )
cout_ << "for user " << *it << " ";
cout_ << ":" << endl;
TQStringList::Iterator sIt = sessions.begin();
for( ; sIt != sessions.end(); ++sIt )
cout_ << " " << *sIt << endl;
cout_ << endl;
}
continue;
}
if( getenv( "DCOPSERVER" ) )
{
sessions.append( getenv( "DCOPSERVER" ) );
presetDCOPServer = true;
}
if( users.count() > 1 || ( users.count() == 1 &&
( getenv( "DCOPSERVER" ) == 0 /*&& getenv( "DISPLAY" ) == 0*/ ) ) )
{
sessions = dcopSessionList( it.key(), it.data() );
if( sessions.isEmpty() )
{
if( users.count() > 1 )
continue;
else
{
cerr_ << "ERROR: No active KDE sessions!" << endl
<< "If you are sure there is one, please set the $DCOPSERVER variable manually" << endl
<< "before calling dcop." << endl;
exit( -1 );
}
}
else if( !sessionName.isEmpty() )
{
if( sessions.contains( sessionName ) )
{
sessions.clear();
sessions.append( sessionName );
}
else
{
cerr_ << "ERROR: The specified session doesn't exist!" << endl;
exit( -1 );
}
}
else if( sessions.count() > 1 && session != AllSessions )
{
cerr_ << "ERROR: Multiple available KDE sessions!" << endl
<< "Please specify the correct session to use with --session or use the" << endl
<< "--all-sessions option to broadcast to all sessions." << endl;
exit( -1 );
}
}
if( users.count() > 1 || ( users.count() == 1 &&
( getenv( "ICEAUTHORITY" ) == 0 || getenv( "DISPLAY" ) == 0 ) ) )
{
// Check for ICE authority file and if the file can be read by us
TQString home = it.data();
TQString iceFile = it.data() + "/.ICEauthority";
TQFileInfo fi( iceFile );
if( iceFile.isEmpty() )
{
cerr_ << "WARNING: Cannot determine home directory for user "
<< it.key() << "!" << endl
<< "Please check permissions or set the $ICEAUTHORITY variable manually before" << endl
<< "calling dcop." << endl;
}
else if( fi.exists() )
{
if( fi.isReadable() )
{
char *envStr = strdup( ( "ICEAUTHORITY=" + iceFile ).ascii() );
putenv( envStr );
//cerr_ << "ice: " << envStr << endl;
}
else
{
cerr_ << "WARNING: ICE authority file " << iceFile
<< "is not readable by you!" << endl
<< "Please check permissions or set the $ICEAUTHORITY variable manually before" << endl
<< "calling dcop." << endl;
}
}
else
{
if( users.count() > 1 )
continue;
else
{
cerr_ << "WARNING: Cannot find ICE authority file "
<< iceFile << "!" << endl
<< "Please check permissions or set the $ICEAUTHORITY"
<< " variable manually before" << endl
<< "calling dcop." << endl;
}
}
}
// Main loop
// If users is an empty list we're calling for the currently logged
// in user. In this case we don't have a session, but still want
// to iterate the loop once.
TQStringList::Iterator sIt = sessions.begin();
for( ; sIt != sessions.end() || users.isEmpty(); ++sIt )
{
if( !presetDCOPServer && !users.isEmpty() )
{
TQString dcopFile = it.data() + "/" + *sIt;
TQFile f( dcopFile );
if( !f.open( IO_ReadOnly ) )
{
cerr_ << "Can't open " << dcopFile << " for reading!" << endl;
exit( -1 );
}
TQStringList l( TQStringList::split( '\n', f.readAll() ) );
dcopServer = l.first();
if( dcopServer.isEmpty() )
{
cerr_ << "WARNING: Unable to determine DCOP server for session "
<< *sIt << "!" << endl
<< "Please check permissions or set the $DCOPSERVER variable manually before" << endl
<< "calling dcop." << endl;
exit( -1 );
}
}
delete client;
client = new DCOPClient;
if( !dcopServer.isEmpty() )
client->setServerAddress( dcopServer.ascii() );
bool success = client->attach();
if( !success )
{
cerr_ << "ERROR: Couldn't attach to DCOP server!" << endl;
retval = QMAX( retval, 1 );
if( users.isEmpty() )
break;
else
continue;
}
dcop = client;
int argscount = args.count();
if ( DCOPrefmode )
argscount++;
switch ( argscount )
{
case 0:
queryApplications("");
break;
case 1:
if (endsWith(app, '*'))
queryApplications(app);
else
queryObjects( app, "" );
break;
case 2:
if (endsWith(objid, '*'))
queryObjects(app, objid);
else
queryFunctions( app, objid );
break;
case 3:
default:
if( updateUserTime )
sendUserTime( app );
if( readStdin )
{
QCStringList::Iterator replaceArg = params.end();
QCStringList::Iterator it = params.begin();
for( ; it != params.end(); ++it )
if( *it == "%1" )
replaceArg = it;
// Read from stdin until EOF and call function for each
// read line
while ( !cin_.atEnd() )
{
TQString buf = cin_.readLine();
if( replaceArg != params.end() )
*replaceArg = buf.local8Bit();
if( !buf.isNull() )
{
int res = callFunction( app, objid, function, params );
retval = QMAX( retval, res );
}
}
}
else
{
// Just call function
// cout_ << "call " << app << ", " << objid << ", " << function << ", (params)" << endl;
int res = callFunction( app, objid, function, params );
retval = QMAX( retval, res );
}
break;
}
// Another sIt++ would make the loop infinite...
if( users.isEmpty() )
break;
}
// Another it++ would make the loop infinite...
if( it == users.end() )
break;
}
return retval;
}
#ifdef Q_OS_WIN
# define main kdemain
#endif
int main( int argc, char** argv )
{
bool readStdin = false;
int numOptions = 0;
TQString user;
Session session = DefaultSession;
TQString sessionName;
bool updateUserTime = true;
cin_.setEncoding( TQTextStream::Locale );
// Scan for command-line options first
for( int pos = 1 ; pos <= argc - 1 ; pos++ )
{
if( strcmp( argv[ pos ], "--help" ) == 0 )
showHelp( 0 );
else if( strcmp( argv[ pos ], "--pipe" ) == 0 )
{
readStdin = true;
numOptions++;
}
else if( strcmp( argv[ pos ], "--user" ) == 0 )
{
if( pos <= argc - 2 )
{
user = TQString::fromLocal8Bit( argv[ pos + 1] );
numOptions +=2;
pos++;
}
else
{
cerr_ << "Missing username for '--user' option!" << endl << endl;
showHelp( -1 );
}
}
else if( strcmp( argv[ pos ], "--session" ) == 0 )
{
if( session == AllSessions )
{
cerr_ << "ERROR: --session cannot be mixed with --all-sessions!" << endl << endl;
showHelp( -1 );
}
else if( pos <= argc - 2 )
{
sessionName = TQString::fromLocal8Bit( argv[ pos + 1] );
numOptions +=2;
pos++;
}
else
{
cerr_ << "Missing session name for '--session' option!" << endl << endl;
showHelp( -1 );
}
}
else if( strcmp( argv[ pos ], "--all-users" ) == 0 )
{
user = "*";
numOptions ++;
}
else if( strcmp( argv[ pos ], "--list-sessions" ) == 0 )
{
session = QuerySessions;
numOptions ++;
}
else if( strcmp( argv[ pos ], "--all-sessions" ) == 0 )
{
if( !sessionName.isEmpty() )
{
cerr_ << "ERROR: --session cannot be mixed with --all-sessions!" << endl << endl;
showHelp( -1 );
}
session = AllSessions;
numOptions ++;
}
else if( strcmp( argv[ pos ], "--no-user-time" ) == 0 )
{
updateUserTime = false;
numOptions ++;
}
else if( argv[ pos ][ 0 ] == '-' )
{
cerr_ << "Unknown command-line option '" << argv[ pos ]
<< "'." << endl << endl;
showHelp( -1 );
}
else
break; // End of options
}
argc -= numOptions;
QCStringList args;
#ifdef DCOPQUIT
if (argc > 1)
{
TQCString prog = argv[ numOptions + 1 ];
if (!prog.isEmpty())
{
args.append( prog );
// Pass as-is if it ends with a wildcard
if (prog[prog.length()-1] != '*')
{
// Strip a trailing -<PID> part.
int i = prog.findRev('-');
if ((i >= 0) && prog.mid(i+1).toLong())
{
prog = prog.left(i);
}
args.append( "qt/"+prog );
args.append( "quit()" );
}
}
}
#else
for( int i = numOptions; i < argc + numOptions - 1; i++ )
args.append( argv[ i + 1 ] );
#endif
if( readStdin && args.count() < 3 )
{
cerr_ << "--pipe option only supported for function calls!" << endl << endl;
showHelp( -1 );
}
if( user == "*" && args.count() < 3 && session != QuerySessions )
{
cerr_ << "ERROR: The --all-users option is only supported for function calls!" << endl << endl;
showHelp( -1 );
}
if( session == QuerySessions && !args.isEmpty() )
{
cerr_ << "ERROR: The --list-sessions option cannot be used for actual DCOP calls!" << endl << endl;
showHelp( -1 );
}
if( session == QuerySessions && user.isEmpty() )
{
cerr_ << "ERROR: The --list-sessions option can only be used with the --user or" << endl
<< "--all-users options!" << endl << endl;
showHelp( -1 );
}
if( session != DefaultSession && session != QuerySessions &&
args.count() < 3 )
{
cerr_ << "ERROR: The --session and --all-sessions options are only supported for function" << endl
<< "calls!" << endl << endl;
showHelp( -1 );
}
UserList users;
if( user == "*" )
users = userList();
else if( !user.isEmpty() )
users[ user ] = userList()[ user ];
int retval = runDCOP( args, users, session, sessionName, readStdin, updateUserTime );
return retval;
}
// vim: set ts=8 sts=4 sw=4 noet: