/* * Copyright 1995, 2000 Perforce Software. All rights reserved. * * This file is part of Perforce - the FAST SCM System. */ /* * ClientUser -- default implementation of user interface for p4 client * * This file implements the p4 client command line interface by defining * the methods of the ClientUser class. This interface reads from stdin, * writes to stdout and stderr, and invokes the user's editor. */ # define NEED_FILE # define NEED_STAT # ifdef OS_NT # include <process.h> # define NEED_FCNTL # ifdef __BORLANDC__ # define ARGVCAST(x) ((char * const *)(x)) # else # define ARGVCAST(x) (x) # endif # endif # include <clientapi.h> # include <clientprog.h> # include <diff.h> # include <enviro.h> # include <echoctl.h> # include <signaler.h> # include <strops.h> # include <runcmd.h> # include <i18napi.h> # include <charcvt.h> # include <msgclient.h> # include <msgserver.h> # ifdef OS_MACOSX # include <CoreFoundation/CoreFoundation.h> # endif int commandChaining = 0; // can be set by clientmain to support '-x run' /* * ClientUser::~ClientUser() - virtual destructor holder */ ClientUser::~ClientUser() { } /* * ClientUser::Prompt() - prompt the user, and wait for a response. */ void ClientUser::Prompt( const StrPtr &msg, StrBuf &buf, int noEcho, Error *e ) { Prompt( msg, buf, noEcho, 0, e ); } void ClientUser::Prompt( const StrPtr &msg, StrBuf &buf, int noEcho, int noOutput, Error *e ) { if ( !noOutput ) printf( "%s", msg.Text() ); fflush( stdout ); int flushStdin = 1; # ifdef OS_NT struct _stat statBuf; _fstat( _fileno( stdin ), &statBuf ); if( statBuf.st_mode != _S_IFCHR ) flushStdin = 0; # endif if( flushStdin ) fflush( stdin ); // Turn off echoing NoEcho *setEcho = noEcho ? new NoEcho : 0; // Prompt'em buf.Clear(); char *b = buf.Alloc( 2048 ); if( !fgets( b, 2048, stdin ) ) { e->Set( MsgClient::Eof ); buf.SetEnd( b ); } else { // Set end and truncate any newline buf.SetEnd( b += strlen( b ) ); # ifdef USE_CR /* * Codewarrior (7.0?) oddity: * fprintf to file translates \n (10) -> \r. * fgets from file translates \n (10) <- \r. * fprintf to console translates \n (10) -> newline on screen. * fgets from stdin DOES NOT translate "enter". */ if( buf.Length() && buf.End()[ -1 ] == '\r' ) buf.End()[ -1 ] = '\n'; # endif if( buf.Length() && buf.End()[ -1 ] == '\n' ) { buf.SetEnd( buf.End() - 1 ); buf.Terminate(); } # ifdef OS_NT if( buf.Length() && buf.End()[ -1 ] == '\r' ) { buf.SetEnd( buf.End() - 1 ); buf.Terminate(); } # endif } delete setEcho; } void ClientUser::Help( const char *const *help ) { for( ; *help; help++ ) printf( "%s\n", *help ); } void ClientUser::HandleError( Error *err ) { // Dumb implementation is just to format // the error and call (old) OutputError. // API callers against 99.1+ servers can do better by // having HandleError probe 'err' directly for detail. StrBuf buf; err->Fmt( buf, EF_NEWLINE ); OutputError( buf.Text() ); } void ClientUser::OutputError( const char *errBuf ) { fflush( stdout ); # ifdef OS_VMS // Note - VMS likes the args this way, Mac the other, // UNIX doesn't care. // fwrite( errBuf, strlen( errBuf ), 1, stderr ); # else fwrite( errBuf, 1, strlen( errBuf ), stderr ); # endif } void ClientUser::OutputInfo( char level, const char *data ) { if( quiet ) return; switch( level ) { default: case '0': break; case '1': printf( "... " ); break; case '2': printf( "... ... " ); break; } # ifdef OS_VMS // Note - VMS likes the args this way, Mac the other, // UNIX doesn't care. // fwrite( data, strlen( data ), 1, stdout ); # else fwrite( data, 1, strlen( data ), stdout ); # endif fputc( '\n', stdout ); } void ClientUser::OutputText( const char *data, int length ) { # ifdef OS_VMS // Note - VMS likes the args this way, Mac the other, // UNIX doesn't care. // fwrite( data, length, 1, stdout ); # else fwrite( data, 1, length, stdout ); # endif } void ClientUser::OutputBinary( const char *data, int length ) { # ifdef OS_NT // we rely on a trailing zero length buffer to // tell us to turn off binary output. if( binaryStdout == !length ) { // toggle binaryStdout = !!length; fflush( stdout ); setmode( fileno( stdout ), binaryStdout ? O_BINARY : O_TEXT ); } # endif fwrite( data, 1, length, stdout ); } void ClientUser::InputData( StrBuf *buf, Error * ) { if( commandChaining ) { for( ;; ) { StrBuf lb; char *b = lb.Alloc( 2048 ); if( !fgets( b, 2048, stdin ) ) break; int l = strlen( b ); if( l > 0 && l <= 3 && b[0] == '.' && ( b[1] == '\r' || b[1] == '\n' ) ) break; buf->Append( b ); } buf->Terminate(); return; } int n; int size = FileSys::BufferSize(); buf->Clear(); do { char *b = buf->Alloc( size ); n = read( 0, b, size ); buf->SetEnd( b + ( n >= 0 ? n : 0 ) ); } while( n > 0 ); buf->Terminate(); } void ClientUser::OutputStat( StrDict *varList ) { int i; StrBuf msg; StrRef var, val; // Dump out the variables, using the GetVar( x ) interface. // Don't display the function (duh), which is only relevant to rpc. // Don't display "specFormatted" which is only relevant to us. for( i = 0; varList->GetVar( i, var, val ); i++ ) { if( var == "func" || var == P4Tag::v_specFormatted ) continue; // otherAction and otherOpen go at level 2, as per 99.1 + earlier msg.Clear(); msg << var << " " << val; char level = strncmp( var.Text(), "other", 5 ) ? '1' : '2'; OutputInfo( level, msg.Text() ); } // blank line OutputInfo( '0', "" ); } void ClientUser::ErrorPause( char *errBuf, Error *e ) { StrBuf buf; OutputError( errBuf ); Prompt( StrRef( "Hit return to continue..." ), buf, 0, e ); if( editFile.Length() > 0 ) { FileSys *f = File( FST_UNICODE ); f->Set( editFile ); f->Unlink( e ); delete f; editFile.Clear(); } } void ClientUser::Edit( FileSys *f1, Error *e ) { Edit( f1, enviro, e ); editFile.Set( f1->Name() ); f1->ClearDeleteOnClose(); } void ClientUser::Edit( FileSys *f1, Enviro * env, Error *e ) { // The presence of $SHELL on NT implies MKS, which has vi. // On VMS, we look for POSIX$SHELL - if set, we'll use vi. // Otherwise, EDIT. if( !f1->IsTextual() ) { e->Set( MsgClient::CantEdit ) << f1->Name(); return; } const char *editor = env->Get( "P4EDITOR" ); if( !editor ) editor = env->Get( "EDITOR" ); # ifdef OS_NT if( !editor && !env->Get( "SHELL" ) ) editor = "notepad"; # endif # ifdef OS_VMS if( !editor && !env->Get( "POSIX$SHELL" ) ) editor = "edit"; # endif # ifdef OS_AS400 if( !editor ) editor = "edtf"; # endif # ifdef OS_MACOSX // open flags: -W wait // -n create a new instance even if one is running // -t use the default text editor // -a use the specified editor if( !editor ) editor = "open -Wnt"; // It's possible P4EDITOR is set to an actual gui application bundle. // Passing bundles to RunCmd won't execute them. // Instead, we can prefix it with "open" so the system // will open it and bring it forward FileSys * editorFile = FileSys::Create( FST_BINARY ); editorFile->Set( editor ); StrBuf openCommand("open -Wna "); // 1 - the file must exist if ( editorFile->Stat() & FSF_EXISTS ) { // 2 - in order to use 'open', the file must also point to a // bundled application. We check to see if there is a bundle // with an executable CFURLRef bundleURL = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *)editor, strlen(editor), true); CFBundleRef bundle = NULL; if ( bundleURL && (bundle = CFBundleCreate(NULL, bundleURL)) ) { CFURLRef executableURL = CFBundleCopyExecutableURL( bundle ); if ( executableURL ) { CFRelease( executableURL ); openCommand.Append( "\"" ); openCommand.Append( editor ); openCommand.Append( "\"" ); editor = openCommand.Text(); } } if ( bundle ) CFRelease( bundle ); if ( bundleURL ) CFRelease( bundleURL ); } # endif // Bill Joy rules, Emacs maggots! if( !editor ) editor = "vi"; RunCmd( editor, f1->Name(), 0, 0, 0, 0, 0, e ); } void ClientUser::Diff( FileSys *f1, FileSys *f2, int doPage, char *df, Error *e ) { Diff(f1, f2, NULL, doPage, df, e); } void ClientUser::Diff( FileSys *f1, FileSys *f2, FileSys *fout, int doPage, char *df, Error *e ) { if( !f1->IsTextual() || !f2->IsTextual() ) { if( f1->Compare( f2, e ) ) { StrRef s( "(... files differ ...)\n" ); if( fout ) { fout->Open( FOM_WRITE, e ); if( !e->Test() ) { fout->Write( s, e ); fout->Close( e ); } } else { printf( "%s", s.Text() ); } } return; } // Call diff to do text compare const char *diffunicode = NULL; const char *diff = enviro->Get( "P4DIFF" ); const char *pager = enviro->Get( "P4PAGER" ); int charset = 0; int output = outputCharset; if( !diff ) diff = enviro->Get( "DIFF" ); if( f1->IsUnicode() ) { diffunicode = enviro->Get( "P4DIFFUNICODE" ); charset = f1->GetContentCharSetPriv(); if( !output && charset == f2->GetContentCharSetPriv() ) output = charset; } if( !doPage ) pager = 0; else if( !pager ) pager = enviro->Get( "PAGER" ); // Do our own diff // VMS ReadFile is busted. # ifdef OS_VMS if( !diff ) diff = "diff"; # else /* not VMS */ if( !diffunicode && !diff ) { // diff expects to read files in raw mode, we must // create new FileSys to allow this. FileSys *f1_bin = File( FST_BINARY ); FileSys *f2_bin = File( FST_BINARY ); int content_charset = f1->GetContentCharSetPriv(); int output_translate = 0; if( f1->IsUnicode() && content_charset != output && content_charset != (int)CharSetApi::UTF_8 ) { // convert files to utf-8 f1_bin->SetDeleteOnClose(); f1_bin->MakeGlobalTemp(); f2_bin->SetDeleteOnClose(); f2_bin->MakeGlobalTemp(); CharSetCvt *cvt; cvt = CharSetCvt::FindCvt( (CharSetApi::CharSet)content_charset, CharSetApi::UTF_8 ); // error out if converter not found? - should not happen f1->Translator( cvt ); f1->Copy( f1_bin, FPM_RW, e ); if( !e->Test() ) { if( cvt ) cvt->ResetErr(); f2->Translator( cvt ); f2->Copy( f2_bin, FPM_RW, e ); } delete cvt; if( output && output != (int)CharSetApi::UTF_8 ) output_translate = 1; } else { if( f1->IsUnicode() && output != content_charset ) output_translate = 1; f1_bin->Set( f1->Name() ); f2_bin->Set( f2->Name() ); } if( !e->Test() ) { DiffFlags flags( df ); # ifndef OS_NEXT :: # endif Diff d; FileSys *t = NULL; d.SetInput( f1_bin, f2_bin, flags, e ); int fileError = e->Test(); if( !fileError || flags.type == DiffFlags::Unified ) { if( fout ) { t = fout; d.SetOutput( t->Name(), e ); } else if( pager || output_translate ) { // this does the work of FileSys::CreateGlobalTemp but // uses the client user object's FileSys create method t = File( FileSysType( ( f1->GetType() & FST_L_MASK ) | FST_UNICODE ) ); t->SetDeleteOnClose(); t->MakeGlobalTemp(); d.SetOutput( t->Name(), e ); } else { d.SetOutput( stdout ); } } if( fileError && flags.type == DiffFlags::Unified ) { d.DiffUnifiedDeleteFile( f1_bin, e ); d.CloseOutput( e ); } else if( fileError ) { d.CloseOutput( e ); } else if( !fileError ) { if( !e->Test() ) d.DiffWithFlags( flags ); d.CloseOutput( e ); if( output_translate ) { // set translator CharSetCvt *cvt; cvt = CharSetCvt::FindCvt( CharSetApi::UTF_8, (CharSetApi::CharSet)output ); // error out if converter not found? t->Translator( cvt ); if( pager ) { FileSys *tr = File( f1->GetType() ); tr->SetDeleteOnClose(); tr->MakeGlobalTemp(); t->Copy( tr, FPM_RW, e ); if( !fout ) delete t; t = tr; } else if( !fout ) { // read the file to stdout... t->Open( FOM_READ, e ); if( !e->Test() ) { char buf[2048]; int i; while( (i = t->Read( buf, sizeof(buf), e )) > 0 && !e->Test() ) fwrite( buf, i, 1, stdout ); t->Close( e ); } } delete cvt; } if( pager && !e->Test() ) RunCmd( pager, t->Name(), 0, 0, 0, 0, 0, e ); if( !fout || pager ) delete t; } } delete f1_bin; delete f2_bin; return; } # endif /* VMS */ // Build up flags args // Yuk. if( !df || !*df ) { if( diffunicode ) RunCmd( diffunicode, CharSetApi::Name( (CharSetApi::CharSet)charset ), f1->Name(), f2->Name(), 0, 0, pager, e ); else RunCmd( diff, f1->Name(), f2->Name(), 0, 0, 0, pager, e ); } else { StrBuf flags; flags.Set( "-", 1 ); flags << df; if( diffunicode ) RunCmd( diffunicode, flags.Text(), CharSetApi::Name( (CharSetApi::CharSet)charset ), f1->Name(), f2->Name(), 0, pager, e ); else RunCmd( diff, flags.Text(), f1->Name(), f2->Name(), 0, 0, pager, e ); } } void ClientUser::Merge( FileSys *base, FileSys *leg1, FileSys *leg2, FileSys *result, Error *e ) { char *merger; if( result->IsUnicode() ) { int cs = result->GetContentCharSetPriv(); if( cs != 0 && ( merger = enviro->Get( "P4MERGEUNICODE" ) ) ) { RunCmd( merger, CharSetApi::Name( (CharSetApi::CharSet)cs ), base->Name(), leg1->Name(), leg2->Name(), result->Name(), 0, e ); return; } } merger = enviro->Get( "P4MERGE" ); if( !merger ) merger = enviro->Get( "MERGE" ); if( !merger ) { e->Set( MsgClient::NoMerger ); return; } RunCmd( merger, base->Name(), leg1->Name(), leg2->Name(), result->Name(), 0, 0, e ); } void ClientUser::RunCmd( const char *command, const char *arg1, const char *arg2, const char *arg3, const char *arg4, const char *arg5, const char *pager, Error *e ) { // XXX RunCommand is dynamically allocated // to work around linux 2.5 24x86 compiler bug. // see job019081. RunCommand *rc = new RunCommand; fflush( stdout ); signaler.Block(); // reset SIGINT to SIGDFL // Use AddCmd() to handle command which may be a mix of // cmd name (with spaces on NT) and flags (following spaces). RunArgs cmd; cmd.AddCmd( command ); if( arg1 ) cmd.AddArg( arg1 ); if( arg2 ) cmd.AddArg( arg2 ); if( arg3 ) cmd.AddArg( arg3 ); if( arg4 ) cmd.AddArg( arg4 ); if( arg5 ) cmd.AddArg( arg5 ); if( pager ) { cmd.AddArg( "|" ); cmd.AddArg( pager ); } rc->Run( cmd, e ); delete rc; signaler.Catch(); // catch SIGINT again } FileSys * ClientUser::File( FileSysType type ) { return FileSys::Create( type ); } int ClientUser::ProgressIndicator() { return 0; } ClientProgress * ClientUser::CreateProgress( int ) { return NULL; } int ClientUserProgress::ProgressIndicator() { return 1; } ClientProgress * ClientUserProgress::CreateProgress( int t ) { return new ClientProgressText( t ); } int ClientUser::Resolve( ClientMerge *m, Error *e ) { // pun from ClientMergeStatus return (int)m->Resolve( e ); } int ClientUser::Resolve( ClientResolveA *r, int preview, Error *e ) { // pun from ClientMergeStatus return (int)r->Resolve( preview, e ); } void ClientUser::Message( Error *err ) { int keepfile = 0; if( err->IsInfo() ) { // Info StrBuf buf; err->Fmt( buf, EF_PLAIN ); OutputInfo( (char)err->GetGeneric() + '0', buf.Text() ); if( err->CheckId( MsgServer::SpecNotCorrect ) ) keepfile = 1; } else { // warn, failed, fatal HandleError( err ); // report file name left if( !err->CheckId( MsgServer::ErrorInSpec ) ) keepfile = 1; } if( editFile.Length() > 0 ) { if( keepfile ) { Error other; other.Set( MsgClient::FileKept ) << editFile.Text(); HandleError( &other ); } else { FileSys *f = File( FST_UNICODE ); f->Set( editFile ); f->Unlink( err ); delete f; } editFile.Clear(); } } void ClientUser::SetOutputCharset( int charset ) { outputCharset = charset; } void ClientUser::DisableTmpCleanup() { signaler.Disable(); } void ClientUser::SetQuiet() { quiet = 1; } int ClientUser::CanAutoLoginPrompt() { // Only allow auto login prompting if: return autoLogin && // it's enabled in the clientuser !quiet && // clientuser isn't in quiet mode isatty( fileno( stdin ) ) && // all STDIO pipes are tty's isatty( fileno( stdout ) ) && isatty( fileno( stderr ) ) ? 1 : 0; }