/*
* 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;
}
int
ClientUser::IsOutputTaggedWithErrorLevel()
{
return outputTaggedWithErrorLevel;
}