/*
* $Id$
*
* Copyright (c) 1996, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.javatest.agent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ConnectException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import com.sun.javatest.Status;
import com.sun.javatest.util.DynamicArray;
/**
* Access to the facilities provided by JT Harness agents.
* Agents provide the ability to run remote and distributed tests.
* The AgentManager provides a single point of control for creating
* connections to agents, for managing a pool of available active agents,
* and for registering observers for interesting events.
**/
public class AgentManager
{
//--------------------------------------------------------------------------
/**
* Access the one single instance of the AgentManager.
* @return The one AgentManager.
*/
public static AgentManager access() {
return theManager;
}
// private, so others can't create additional managers
private AgentManager() {
}
private static final AgentManager theManager = new AgentManager();
//--------------------------------------------------------------------------
/**
* An Observer class to monitor Agent activity.
*/
public static interface Observer {
/**
* Called when a task starts a request.
* @param connection The connection used to communicate with the agent.
* @param tag A tag used to identify the request.
* @param request The type of the request.
* @param executable The class to be executed.
* @param args Arguments to be passed to the class to be executed.
* @param localizeArgs Whether or not to localize the args remotely, using the
* agent's map facility.
*/
void started(Connection connection,
String tag, String request, String executable, String[] args,
boolean localizeArgs);
/**
* Called when a task completes a request.
* @param connection The connection used to communicate with the agent.
* @param status The outcome of the request.
*/
void finished(Connection connection, Status status);
}
/**
* Add an observer to monitor events.
* @param o The observer to be added.
* @see #removeObserver
*/
public synchronized void addObserver(Observer o) {
observers = DynamicArray.append(observers, o);
}
/**
* Remove an observer that had been previously registered to monitor events.
* @param o The observer to be removed.
*/
public synchronized void removeObserver(Observer o) {
observers = DynamicArray.remove(observers, o);
}
private synchronized void notifyStarted(Connection connection,
String tag, String request, String executable, String[] args,
boolean localizeArgs) {
for (int i = 0; i < observers.length; i++) {
observers[i].started(connection, tag, request, executable, args, localizeArgs);
}
}
private synchronized void notifyFinished(Connection connection, Status status) {
for (int i = 0; i < observers.length; i++) {
observers[i].finished(connection, status);
}
}
private Observer[] observers = new Observer[0];
//--------------------------------------------------------------------------
/**
* Get the active agent pool, which is a holding area for
* agents that have registered themselves as available for use
* by the test harness.
* @return the active agent pool
*/
public ActiveAgentPool getActiveAgentPool() {
return pool;
}
private ActiveAgentPool pool = new ActiveAgentPool();
//--------------------------------------------------------------------------
/**
* Create a task that will connect with an agent via a given connection.
* @param c The connection to use to communicate with the agent.
* @return a task object that can be used to initiate work on an agent
* @throws IOException if a problem occurs establishing the connection
*/
public Task connect(Connection c) throws IOException {
return new Task(c);
}
/**
* Create a task that will connect to the next available active agent
* that is available in a central pool.
* @return a task object that can be used to initiate work on an agent
* @throws ActiveAgentPool.NoAgentException if the pool has not been
* initialized, or if there is still no agent after
* a timeout period specified when the pool was initialized.
* @throws InterruptedException if this thread is interrupted while waiting
* for an agent to become available in the active agent pool.
* @throws IOException if a problem occurs establishing the connection
*/
public Task connectToActiveAgent() throws ActiveAgentPool.NoAgentException, InterruptedException, IOException {
return connect(pool.nextAgent());
}
/**
* Create a task that will connect to a passive agent running on a
* nominated host, and which is listening on the default port.
* @param host The host on which the agent should be running.
* @return a task object that can be used to initiate work on an agent
* @throws IOException is there is a problem connecting to the agent
* @throws NullPointerException if host is null
*/
public Task connectToPassiveAgent(String host) throws IOException {
if (host == null) {
throw new NullPointerException();
}
return connectToPassiveAgent(host, Agent.defaultPassivePort);
}
/**
* Create a connection to a passive agent running on a nominated host,
* and which is listening on a specified port.
* @param host The host on which the agent should be running.
* @param port The port on which the agent should be listening.
* @return a task object that can be used to initiate work on an agent
* @throws IOException is there is a problem connecting to the agent
* @throws NullPointerException if host is null
*/
public Task connectToPassiveAgent(String host, int port) throws IOException {
if (host == null) {
throw new NullPointerException();
}
for (int i = 0; ; i++) {
try {
// return connect(new SocketConnection(host, port));
return connect(new InterruptableSocketConnection(host, port));
}
catch (ConnectException e) {
if (i == PASSIVE_AGENT_RETRY_LIMIT) {
throw e;
}
try {
Thread.currentThread().sleep(5000);
}
catch (InterruptedException ignore) {
}
}
}
}
private static final int PASSIVE_AGENT_RETRY_LIMIT = 12;
//--------------------------------------------------------------------------
/**
* A Task provides the ability to do work remotely on an agent.
*/
public class Task {
/**
* Create a connection to a agent retrieved from the agent pool.
* @param c The connection with which to communicate to the agent.
*/
Task(Connection c) throws IOException {
connection = c;
in = new DataInputStream(new BufferedInputStream(c.getInputStream()));
out = new DataOutputStream(new BufferedOutputStream(c.getOutputStream()));
}
/**
* Get the connection being used for this task.
* @return the connection to the remote agent.
*/
public Connection getConnection() {
return connection;
}
/**
* Get the classpath to be used when loading classes on behalf of the
* remote agent.
* @return An array of files and directories in which to look for classes to
* be given to the remote agent.
* @see #setClassPath(String)
* @see #setClassPath(File[])
*/
public File[] getClassPath() {
return classPath;
}
/**
* Set the classpath to be used for incoming requests for classes
* from the remote agent.
* @param path The classpath to be set, using the local
* file and path separator characters.
* @see #getClassPath
* @see #setClassPath(File[])
*/
public void setClassPath(String path) {
classPath = split(path);
}
/**
* Set the classpath to be used for incoming requests for classes
* from the remote agent.
* @param path An array of files, representing the path to be used.
* @see #getClassPath
* @see #setClassPath(String)
*/
public void setClassPath(File[] path) {
if (path == null) {
throw new NullPointerException();
}
for (int i = 0; i < path.length; i++) {
if (path[i] == null) {
throw new IllegalArgumentException();
}
}
classPath = path;
}
/**
* Set the shared classloader mode to be used for incoming requests for
* classes from the remote agent.
*
* @param state The shared classloader mode, true to use a shared loader.
*/
public void setSharedClassLoader(boolean state) {
this.sharedCl = state;
}
/**
* Set the timeout after command thread will be interrupted on the agent
* @param timeout value in seconds
*/
public void setAgentCommandTimeout(int timeout) {
this.timeout = timeout;
}
/**
* Request the agent for this client to execute a standard Test class.
* @param tag A string to identify the request in debug and trace output.
* @param className The name of the class to execute. The class must be an
* implementation of com.sun.javatest.Test, and must be
* accessible by the agent: just the name of the class
* is sent to the agent, not the classfile.
* @param args The arguments to be passed to the run
method
* of an instance of classname
that will be executed
* by the agent.
* @param localizeArgs
* Whether or not to instruct the agent to localize the args
* to be passed to the test class. For example, this may be
* necessary if the test has arguments that involve filenames
* that differ from system to system.
* @param log A stream to which to write any data written to the log
* stream when the test class is run.
* @param ref A stream to which to write any data written to the ref
* stream when the test class is run.
* @return The status returned when the test class is run by the agent.
* @see com.sun.javatest.Test
*/
public Status executeTest(String tag, String className, String[] args,
boolean localizeArgs,
PrintWriter log, PrintWriter ref) {
return run(tag, "executeTest", className, args, localizeArgs, log, ref);
}
/**
* Request the agent for this client to execute a standard Command class.
* @param tag A string to identify the request in debug and trace output.
* @param className The name of the class to execute. The class must be an
* implementation of com.sun.javatest.Command, and must be
* accessible by the agent: just the name of the class
* is sent to the agent, not the classfile.
* @param args The arguments to be passed to the run
method
* of an instance of classname
that will be executed
* by the agent.
* @param localizeArgs
* Whether or not to instruct the agent to localize the args
* to be passed to the test class. For example, this may be
* necessary if the test has arguments that involve filenames
* that differ from system to system.
* @param log A stream to which to write any data written to the log
* stream when the command class is run.
* @param ref A stream to which to write any data written to the ref
* stream when the command class is run.
* @return The status returned when the command class is run by the agent.
* @see com.sun.javatest.Command
*/
public Status executeCommand(String tag, String className, String[] args,
boolean localizeArgs,
PrintWriter log, PrintWriter ref) {
return run(tag, "executeCommand", className, args, localizeArgs, log, ref);
}
/**
* Request the agent for this client to execute the standard "main" method
* for a class.
* @param tag A string to identify the request in debug and trace output.
* @param className The name of the class to execute. The class must be an
* implementation of com.sun.javatest.Command, and must be
* accessible by the agent: just the name of the class
* is sent to the agent, not the classfile.
* @param args The arguments to be passed to the run
method
* of an instance of classname
that will be executed
* by the agent.
* @param localizeArgs
* Whether or not to instruct the agent to localize the args
* to be passed to the test class. For example, this may be
* necessary if the test has arguments that involve filenames
* that differ from system to system.
* @param log A stream to which to write any data written to the log
* stream when the command class is run.
* @param ref A stream to which to write any data written to the ref
* stream when the command class is run.
* @return Status.passed if the method terminated normally;
* Status.failed if the method threw an exception;
* Status.error if some other problem arose.
* @see com.sun.javatest.Command
*/
public Status executeMain(String tag, String className, String[] args,
boolean localizeArgs,
PrintWriter log, PrintWriter ref) {
return run(tag, "executeMain", className, args, localizeArgs, log, ref);
}
private Status run(String tag, String request, String executable, String[] args,
boolean localizeArgs,
PrintWriter log, PrintWriter ref) {
notifyStarted(connection, tag, request, executable, args, localizeArgs);
Status result = null;
try {
// boolean sharedClOption = false;
out.writeShort(Agent.protocolVersion);
out.writeUTF(tag);
out.writeUTF(request);
out.writeUTF(executable);
out.writeShort(args.length);
for (int i = 0; i < args.length; i++) {
out.writeUTF(args[i]);
// if ("-sharedCl".equalsIgnoreCase(args[i]) ||
// "-sharedClassLoader".equalsIgnoreCase(args[i])) {
// sharedClOption = true;
// }
}
out.writeBoolean(localizeArgs);
out.writeBoolean(classPath != null); // specify remoteClasses if classPath has been given
out.writeBoolean(sharedCl);
out.writeInt(timeout);
out.writeByte(0);
out.flush();
result = readResults(log, ref);
}
catch (IOException e) {
try {
if (out != null)
out.close();
if (in != null)
in.close();
}
catch (IOException ignore) {
}
if (e instanceof InterruptedIOException)
result = Status.error("Communication with agent interrupted! (timed out?)." +
"\n InterruptedException: " + e);
else
result = Status.error("Problem communicating with agent: " + e);
}
finally {
notifyFinished(connection, result);
}
return result;
}
private Status readResults(PrintWriter log, PrintWriter ref)
throws IOException
{
Status status = null;
while (status == null) {
int code = in.read();
switch (code) {
case -1: // unexpected EOF
status = Status.error("premature EOF from agent");
break;
case Agent.CLASS:
String className = in.readUTF();
//System.err.println("received request for " + className);
AgentRemoteClassData classData = locateClass(className);
classData.write(out);
out.flush();
break;
case Agent.DATA:
String resourceName = in.readUTF();
//System.err.println("received request for " + resourceName);
byte[] resourceData = locateData(resourceName);
if (resourceData == null)
//System.err.println("resource not found: " + className);
out.writeInt(-1);
else {
out.writeInt(resourceData.length);
out.write(resourceData, 0, resourceData.length);
}
out.flush();
//System.err.println("done request for " + resourceName);
break;
case Agent.STATUS:
int type = in.read();
String reason = in.readUTF();
switch (type) {
case Status.PASSED:
status = Status.passed(reason);
break;
case Status.FAILED:
status = Status.failed(reason);
break;
case Status.ERROR:
status = Status.error(reason);
break;
default:
status = Status.failed("Bad status from test: type=" + type + " reason=" + reason);
break;
}
break;
case Agent.LOG:
log.write(in.readUTF());
break;
case Agent.LOG_FLUSH:
log.write(in.readUTF());
log.flush();
break;
case Agent.REF:
ref.write(in.readUTF());
break;
case Agent.REF_FLUSH:
ref.write(in.readUTF());
ref.flush();
break;
}
}
out.close();
in.close();
connection.close();
log.flush();
ref.flush();
// might be better not to flush these ...
for (Enumeration e = zips.keys(); e.hasMoreElements(); ) {
File f = (e.nextElement());
ZipFile z = zips.get(f);
zips.remove(f);
z.close();
}
return status;
}
private AgentRemoteClassData locateClass(String name) {
//System.err.println("locateClass: " + name);
if (classPath != null) {
String cname = name.replace('.', '/') + ".class";
for (int i = 0; i < classPath.length; i++) {
byte[] data;
if (classPath[i].isDirectory())
data = readFromDir(cname, classPath[i]);
else
data = readFromJar(cname, classPath[i]);
if (data != null) {
String codeSource = "";
try {
codeSource = classPath[i].toURI().toURL().getPath();
} catch (IOException e) {
//codeSource will not be set
}
AgentRemoteClassData classData = new AgentRemoteClassData(name, codeSource, data);
return classData;
}
}
}
return AgentRemoteClassData.NO_CLASS_DATA;
}
private byte[] locateData(String name) {
//System.err.println("locateData: " + name);
if (classPath != null) {
for (int i = 0; i < classPath.length; i++) {
byte[] data;
if (classPath[i].isDirectory())
data = readFromDir(name, classPath[i]);
else
data = readFromJar(name, classPath[i]);
if (data != null)
return data;
}
}
return null;
}
private byte[] readFromDir(String name, File dir) {
//System.err.println("readFromDir: " + name + " " + dir);
try {
File file = new File(dir, name);
return read(new FileInputStream(file), ((int) file.length()));
}
catch (IOException e) {
//System.err.println("readFromDir: " + e);
return null;
}
}
private byte[] readFromJar(String name, File jarFile) {
//System.err.println("readFromJar: " + name + " " + jarFile);
try {
ZipFile z = zips.get(jarFile);
if (z == null) {
z = new ZipFile(jarFile);
zips.put(jarFile, z);
}
ZipEntry ze = z.getEntry(name);
if (ze == null)
return null;
return read(z.getInputStream(ze), ((int) ze.getSize()));
}
catch (IOException e) {
//System.err.println("readFromJar: " + e);
return null;
}
}
private byte[] read(InputStream in, int size) throws IOException {
//System.err.println("read: " + size);
try {
byte data[] = new byte[size];
for (int total = 0; total < data.length; ) {
int n = in.read(data, total, data.length - total);
if (n > 0)
total += n;
else
throw new EOFException("unexpected end of file");
}
//System.err.println("read complete: " + data.length);
return data;
}
finally {
in.close();
}
}
private File[] split(String s) {
char pathCh = File.pathSeparatorChar;
Vector v = new Vector<>();
int start = 0;
for (int i = s.indexOf(pathCh); i != -1; i = s.indexOf(pathCh, start)) {
add(s.substring(start, i), v);
start = i + 1;
}
if (start != s.length())
add(s.substring(start), v);
File[] path = new File[v.size()];
v.copyInto(path);
return path;
}
private void add(String s, Vector v) {
if (s.length() != 0)
v.addElement(new File(s));
}
private Connection connection;
private DataInputStream in;
private DataOutputStream out;
private File[] classPath;
private boolean sharedCl;
private int timeout = 0;
private Hashtable zips = new Hashtable<>();
}
}