package com.perforce.api;
import java.io.*;
import java.util.*;
import java.text.DateFormat;
/*
* Copyright (c) 2001, Perforce Software, All rights reserved.
*
* 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 OR COPYRIGHT HOLDERS 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.
*/
/**
* Handles the execution of all perforce commands. This class can be used
* directly, but the preferred use of this API is through the
* {@link com.perforce.api.SourceControlObject SourceControlObject} subclasses.
* <p>
* <b>Example Usage:</b>
*
* <pre>
* String l;
* Env env = new Env();
* String[] cmd = { "p4", "branches" };
* try {
* P4Process p = new P4Process(env);
* p.exec(cmd);
* while(null != (l = p.readLine())) {
* // Parse the output.
* }
* p.close();
* } catch(Exception ex) {
* throw new PerforceException(ex.getMessage());
* }
* </pre>
*
* @author <a href="mailto:david@markley.cc">David Markley</a>
* @version $Date: 2002/01/15 $ $Revision: #3 $
* @see Env
* @see SourceControlObject
* @see Thread
*/
public class P4Process {
private static P4Process base = null;
private P4JNI jni_proc = null;
private boolean using_native = false;
private Env environ = null;
private Runtime rt = Runtime.getRuntime();
private Process p;
private BufferedReader in, err;
private Writer out;
private int exit_code = 0;
private EventLog log;
private String P4_ERROR = null;
private String[] new_cmd;
private long threshold = 10000; // The default is 10 seconds;
private boolean raw = false;
/**
* Default no-argument constructor. If the runtime has not been established,
* this constructor will set it up. No environment is specified, so the base
* environment will be used if it exists.
*
* @see #getBase()
*/
public P4Process() {
this(null);
}
/**
* Constructor that specifies the source control environment.
*
* @param e
* Source control environment to use.
*/
public P4Process(Env e) {
super();
if(null == rt) {
rt = Runtime.getRuntime();
}
if(null == e) {
if(null == base) {
base = this;
this.environ = new Env();
} else {
this.environ = new Env(base.getEnv());
}
} else {
this.environ = e;
}
if(null != environ)
this.threshold = environ.getServerTimeout();
}
/**
* Sets the environment to use.
*
* @param e
* Source control environment.
*/
public void setEnv(Env e) {
this.environ = e;
if(null != environ)
this.threshold = environ.getServerTimeout();
}
/**
* Returns the environment in use by this process.
*
* @return Source control environment.
*/
public Env getEnv() {
return this.environ;
}
/**
* Returns the base process for this class. The base process is set when
* this class is first instantiated. The base process is used when other
* <code>P4Process</code> are instantiated to share settings, including
* the {@link com.perforce.api.Env source control environment}.
*
* @see Env
* @return Source control environment.
*/
public static P4Process getBase() {
if(null != base) {
return base;
} else {
return new P4Process();
}
}
/**
* Sets the base process to be used when new processes are instantiated.
*
* @see #getBase()
*/
public static void setBase(P4Process b) {
if(null != b) {
base = b;
}
}
public Writer getWriter() {
return out;
}
/**
* Returns the exit code returned when the underlying process exits.
*
* @return Typical UNIX style return code.
*/
public int getExitCode() {
return exit_code;
}
/**
* In raw mode, the process will return the prefix added by the "-s" command
* line option. The default is false.
*/
public void setRawMode(boolean raw) {
this.raw = raw;
}
/**
* Returns the status of raw mode for this process.
*/
public boolean getRawMode() {
return this.raw;
}
/**
* Executes a p4 command. This uses the class environment information to
* execute the p4 command specified in the String array. This array contains
* all the command line arguments that will be specified for execution,
* including "p4" in the first position.
*
* @param cmd
* Array of command line arguments ("p4" must be first).
*/
public synchronized void exec(String[] cmd) throws IOException {
String[] pre_cmds = new String[12];
int i = 0;
pre_cmds[i++] = cmd[0];
pre_cmds[i++] = "-s";// Forces all commands to use stdout for message
// reporting, no longer read stderr
if(!getEnv().getPort().trim().equals("")) {
pre_cmds[i++] = "-p";
pre_cmds[i++] = getEnv().getPort();
}
if(!getEnv().getUser().trim().equals("")) {
pre_cmds[i++] = "-u";
pre_cmds[i++] = getEnv().getUser();
}
if(!getEnv().getClient().trim().equals("")) {
pre_cmds[i++] = "-c";
pre_cmds[i++] = getEnv().getClient();
}
if(!getEnv().getPassword().trim().equals("")) {
pre_cmds[i++] = "-P";
pre_cmds[i++] = getEnv().getPassword();
}
if(cmd[1].equals("-x")) {
pre_cmds[i++] = "-x";
pre_cmds[i++] = cmd[2];
}
new_cmd = new String[(i + cmd.length) - 1];
for(int j = 0; j < (i + cmd.length) - 1; j++) {
if(j < i) {
new_cmd[j] = pre_cmds[j];
} else {
new_cmd[j] = cmd[(j - i) + 1];
}
}
Debug.verbose("P4Process.exec: ", new_cmd);
if(P4JNI.isValid()) {
native_exec(new_cmd);
using_native = true;
} else {
pure_exec(new_cmd);
using_native = false;
}
}
/**
* Executes the command utilizing the P4API. This method will be used only
* if the supporting Java Native Interface library could be loaded.
*/
private synchronized void native_exec(String[] cmd) throws IOException {
jni_proc = new P4JNI();
// P4JNI tmp = new P4JNI();
jni_proc.runCommand(jni_proc, cmd, environ);
in = jni_proc.getReader();
err = in;
out = jni_proc.getWriter();
}
/**
* Executes the command through a system 'exec'. This method will be used
* only if the supporting Java Native Interface library could not be loaded.
*/
private synchronized void pure_exec(String[] cmd) throws IOException {
if(null != this.environ.getExecutable()) {
cmd[0] = this.environ.getExecutable();
}
p = rt.exec(cmd, this.environ.getEnvp());
InputStream is = p.getInputStream();
Debug.verbose("P4Process.exec().is: " + is);
InputStreamReader isr = new InputStreamReader(is);
Debug.verbose("P4Process.exec().isr: " + isr);
in = new BufferedReader(isr);
InputStream es = p.getErrorStream();
Debug.verbose("P4Process.exec().es: " + es);
InputStreamReader esr = new InputStreamReader(es);
Debug.verbose("P4Process.exec().esr: " + esr);
err = new BufferedReader(esr);
OutputStream os = p.getOutputStream();
Debug.verbose("P4Process.exec().os: " + os);
OutputStreamWriter osw = new OutputStreamWriter(os);
Debug.verbose("P4Process.exec().osw: " + osw);
out = new FilterWriter(new BufferedWriter(osw)) {
public void write(String str) throws IOException {
super.write(str);
System.out.print("P4DebugOutput: " + str);
}
};
}
/**
* Sets the event log. Any events that should be logged will be logged
* through the EventLog specified here.
*
* @param log
* Log for all events.
*/
public synchronized void setEventLog(EventLog log) {
this.log = log;
}
/**
* Logs the event message to the output stream.
*
* @param out
* Stream to which the message is logged.
* @param event
* Message to be logged.
*/
private void log(PrintStream out, String event) {
if(null == log) {
out.println(event);
out.flush();
} else {
log.log(event);
}
}
/**
* Writes <code>line</code> to the standard input of the process.
*
* @param line
* Line to be written.
*/
public synchronized void print(String line) throws IOException {
out.write(line);
}
/**
* Writes <code>line</code> to the standard input of the process. A
* newline is appended to the output.
*
* @param line
* Line to be written.
*/
public synchronized void println(String line) throws IOException {
out.write(line + "\n");
}
/**
* Flushes the output stream to the process.
*/
public synchronized void flush() throws IOException {
out.flush();
}
/**
* Flushes and closes the output stream to the process.
*/
public synchronized void outClose() throws IOException {
out.flush();
out.close();
}
/**
* Returns the next line from the process, or null if the command has
* completed its execution.
*/
public synchronized String readLine() {
if(using_native && null != jni_proc && jni_proc.isPiped()) {
return native_readLine();
} else {
return pure_readLine();
}
}
/**
* Reads the next line from the process. This method will be used only if
* the supporting Java Native Interface library could be loaded.
*/
private synchronized String native_readLine() {
try {
return in.readLine();
} catch(IOException ex) {
return null;
}
}
/**
* Reads the next line from the process. This method will be used only if
* the supporting Java Native Interface library could not be loaded.
*/
private synchronized String pure_readLine() {
String line;
long current, timeout = ((new Date()).getTime()) + threshold;
if(null == p || null == in || null == err)
return null;
// Debug.verbose("P4Process.readLine()");
try {
for(;;) {
if(null == p || null == in || null == err) {
Debug.error("P4Process.readLine(): Something went null");
return null;
}
current = (new Date()).getTime();
if(current >= timeout) {
Debug.error("P4Process.readLine(): Timeout");
// If this was generating a new object from stdin, return an
// empty string. Otherwise, return null.
for(int i = 0; i < new_cmd.length; i++) {
if(new_cmd[i].equals("-i"))
return "";
}
return null;
}
// Debug.verbose("P4Process.readLine().in: "+in);
try {
/**
* If there's something coming in from stdin, return it. We
* assume that the p4 command was called with -s which sends
* all messages to standard out pre-pended with a string
* that indicates what kind of messsage it is error warning
* text info exit
*/
// Some errors still come in on Standard error
while(err.ready()) {
line = err.readLine();
if(null != line) {
addP4Error(line + "\n");
}
}
if(in.ready()) {
line = in.readLine();
Debug.verbose("From P4:" + line);
if(line.startsWith("error")) {
if(!line.trim().equals("") && (-1 == line.indexOf("up-to-date"))
&& (-1 == line.indexOf("no file(s) to resolve"))) {
addP4Error(line);
}
} else if(line.startsWith("warning")) {
} else if(line.startsWith("text")) {
} else if(line.startsWith("info")) {
} else if(line.startsWith("exit")) {
int exit_code = new Integer(line.substring(line.indexOf(" ") + 1, line.length()))
.intValue();
if(0 == exit_code) {
Debug.verbose("P4 Exec Complete.");
} else {
Debug.error("P4 exited with an Error!");
}
return null;
}
if(!raw)
line = line.substring(line.indexOf(":") + 1).trim();
Debug.verbose("P4Process.readLine(): " + line);
return line;
}
} catch(NullPointerException ne) {
}
// If there's nothing on stdin or stderr, check to see if the
// process has exited. If it has, return null.
try {
exit_code = p.exitValue();
return null;
} catch(IllegalThreadStateException ie) {
Debug.verbose("P4Process: Thread is not done yet.");
}
// Sleep for a second, so this thread can't become a CPU hog.
try {
Debug.verbose("P4Process: Sleeping...");
Thread.sleep(100); // Sleep for 1/10th of a second.
} catch(InterruptedException ie) {
}
}
} catch(IOException ex) {
return null;
}
}
/**
* Waits for the process to exit and closes out the process. This method
* should be called after the {@link #exec(java.lang.String[]) exec} method
* in order to close things down properly.
*
* @param out
* The stream to which any errors should be sent.
* @return The exit value of the underlying process.
*/
public synchronized int close(PrintStream out) throws IOException {
if(using_native && null != jni_proc && jni_proc.isPiped()) {
native_close(out);
} else {
pure_close(out);
}
/*
* if (0 != exit_code) { throw new IOException("P4Process ERROR: p4 sync
* exited with error ("+ exit_code+")"); }
*/
if(null != P4_ERROR) {
throw new IOException(P4_ERROR);
}
return exit_code;
}
/**
* Closes down connections to the underlying process. This method will be
* used only if the supporting Java Native Interface library could be
* loaded.
*/
private synchronized void native_close(PrintStream out) {
try {
in.close();
out.flush();
out.close();
} catch(IOException ioe) {
}
}
/**
* Closes down connections to the underlying process. This method will be
* used only if the supporting Java Native Interface library could not be
* loaded.
*/
private synchronized void pure_close(PrintStream out) {
/*
* Try to close this process for at least 30 seconds.
*/
for(int i = 0; i < 30; i++) {
try {
in.close();
err.close();
out.flush();
out.close();
} catch(IOException ioe) {
}
try {
exit_code = p.waitFor();
p.destroy();
break;
} catch(InterruptedException ie) {
}
try {
Thread.sleep(1000);
} catch(InterruptedException ie) {
}
}
}
/**
* Waits for the underlying process to exit and closes it down. This method
* should be called after the {@link #exec(java.lang.String[]) exec} method
* in order to close things out properly. Errors are sent to System.err.
*
* @see System
* @return The exit value of the underlying process.
*/
public int close() throws IOException {
return close(System.err);
}
/** Set the server timeout threshold. */
public void setServerTimeout(long threshold) {
this.threshold = threshold;
}
/** Return the server timeout threshold. */
public long getServerTimeout() {
return threshold;
}
public String toString() {
return this.environ.toString();
}
private void addP4Error(String message) {
if(null == P4_ERROR) {
P4_ERROR = message;
} else {
P4_ERROR += message;
}
}
}