1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 1996, 2018, Oracle and/or its affiliates. All rights reserved.
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * This code is free software; you can redistribute it and/or modify it
   8  * under the terms of the GNU General Public License version 2 only, as
   9  * published by the Free Software Foundation.  Oracle designates this
  10  * particular file as subject to the "Classpath" exception as provided
  11  * by Oracle in the LICENSE file that accompanied this code.
  12  *
  13  * This code is distributed in the hope that it will be useful, but WITHOUT
  14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  16  * version 2 for more details (a copy is included in the LICENSE file that
  17  * accompanied this code).
  18  *
  19  * You should have received a copy of the GNU General Public License version
  20  * 2 along with this work; if not, write to the Free Software Foundation,
  21  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  22  *
  23  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  24  * or visit www.oracle.com if you need additional information or have any
  25  * questions.
  26  */
  27 package com.sun.javatest.agent;
  28 
  29 import java.io.BufferedInputStream;
  30 import java.io.BufferedOutputStream;
  31 import java.io.DataInputStream;
  32 import java.io.DataOutputStream;
  33 import java.io.EOFException;
  34 import java.io.File;
  35 import java.io.FileInputStream;
  36 import java.io.InputStream;
  37 import java.io.InterruptedIOException;
  38 import java.io.IOException;
  39 import java.io.PrintWriter;
  40 import java.net.ConnectException;
  41 import java.util.Enumeration;
  42 import java.util.Hashtable;
  43 import java.util.Vector;
  44 import java.util.zip.ZipEntry;
  45 import java.util.zip.ZipFile;
  46 
  47 import com.sun.javatest.Status;
  48 import com.sun.javatest.util.DynamicArray;
  49 
  50 /**
  51  * Access to the facilities provided by JT Harness agents.
  52  * Agents provide the ability to run remote and distributed tests.
  53  * The AgentManager provides a single point of control for creating
  54  * connections to agents, for managing a pool of available active agents,
  55  * and for registering observers for interesting events.
  56  **/
  57 public class AgentManager
  58 {
  59 
  60     //--------------------------------------------------------------------------
  61 
  62     /**
  63      * Access the one single instance of the AgentManager.
  64      * @return The one AgentManager.
  65      */
  66     public static AgentManager access() {
  67         return theManager;
  68     }
  69 
  70     // private, so others can't create additional managers
  71     private AgentManager() {
  72     }
  73 
  74     private static final AgentManager theManager = new AgentManager();
  75 
  76     //--------------------------------------------------------------------------
  77 
  78     /**
  79      * An Observer class to monitor Agent activity.
  80      */
  81     public static interface Observer {
  82         /**
  83          * Called when a task starts a request.
  84          * @param connection    The connection used to communicate with the agent.
  85          * @param tag           A tag used to identify the request.
  86          * @param request       The type of the request.
  87          * @param executable    The class to be executed.
  88          * @param args          Arguments to be passed to the class to be executed.
  89          * @param localizeArgs  Whether or not to localize the args remotely, using the
  90          *                      agent's map facility.
  91          */
  92         void started(Connection connection,
  93                      String tag, String request, String executable, String[] args,
  94                      boolean localizeArgs);
  95 
  96         /**
  97          * Called when a task completes a request.
  98          * @param connection    The connection used to communicate with the agent.
  99          * @param status        The outcome of the request.
 100          */
 101         void finished(Connection connection, Status status);
 102     }
 103 
 104     /**
 105      * Add an observer to monitor events.
 106      * @param o         The observer to be added.
 107      * @see #removeObserver
 108      */
 109     public synchronized void addObserver(Observer o) {
 110         observers = DynamicArray.append(observers, o);
 111     }
 112 
 113     /**
 114      * Remove an observer that had been previously registered to monitor events.
 115      * @param o         The observer to be removed.
 116      */
 117     public synchronized void removeObserver(Observer o) {
 118         observers = DynamicArray.remove(observers, o);
 119     }
 120 
 121     private synchronized void notifyStarted(Connection connection,
 122                                             String tag, String request, String executable, String[] args,
 123                                             boolean localizeArgs) {
 124         for (int i = 0; i < observers.length; i++) {
 125             observers[i].started(connection, tag, request, executable, args, localizeArgs);
 126         }
 127     }
 128 
 129     private synchronized void notifyFinished(Connection connection, Status status) {
 130         for (int i = 0; i < observers.length; i++) {
 131             observers[i].finished(connection, status);
 132         }
 133     }
 134 
 135     private Observer[] observers = new Observer[0];
 136 
 137     //--------------------------------------------------------------------------
 138 
 139     /**
 140      * Get the active agent pool, which is a holding area for
 141      * agents that have registered themselves as available for use
 142      * by the test harness.
 143      * @return the active agent pool
 144      */
 145     public ActiveAgentPool getActiveAgentPool() {
 146         return pool;
 147     }
 148 
 149     private ActiveAgentPool pool = new ActiveAgentPool();
 150 
 151     //--------------------------------------------------------------------------
 152 
 153     /**
 154      * Create a task that will connect with an agent via a given connection.
 155      * @param c The connection to use to communicate with the agent.
 156      * @return a task object that can be used to initiate work on an agent
 157      * @throws IOException if a problem occurs establishing the connection
 158      */
 159     public Task connect(Connection c) throws IOException {
 160         return new Task(c);
 161     }
 162 
 163     /**
 164      * Create a task that will connect to the next available active agent
 165      * that is available in a central pool.
 166      * @return a task object that can be used to initiate work on an agent
 167      * @throws ActiveAgentPool.NoAgentException if the pool has not been
 168      *                  initialized, or if there is still no agent after
 169      *                  a timeout period specified when the pool was initialized.
 170      * @throws InterruptedException if this thread is interrupted while waiting
 171      *                  for an agent to become available in the active agent pool.
 172      * @throws IOException if a problem occurs establishing the connection
 173      */
 174     public Task connectToActiveAgent() throws ActiveAgentPool.NoAgentException, InterruptedException, IOException {
 175         return connect(pool.nextAgent());
 176     }
 177 
 178     /**
 179      * Create a task that will connect to a passive agent running on a
 180      * nominated host, and which is listening on the default port.
 181      * @param host      The host on which the agent should be running.
 182      * @return a task object that can be used to initiate work on an agent
 183      * @throws IOException is there is a problem connecting to the agent
 184      * @throws NullPointerException if host is null
 185      */
 186     public Task connectToPassiveAgent(String host) throws IOException {
 187         if (host == null) {
 188             throw new NullPointerException();
 189         }
 190 
 191         return connectToPassiveAgent(host, Agent.defaultPassivePort);
 192     }
 193 
 194 
 195     /**
 196      * Create a connection to a passive agent running on a nominated host,
 197      * and which is listening on a specified port.
 198      * @param host      The host on which the agent should be running.
 199      * @param port      The port on which the agent should be listening.
 200      * @return a task object that can be used to initiate work on an agent
 201      * @throws IOException is there is a problem connecting to the agent
 202      * @throws NullPointerException if host is null
 203      */
 204     public Task connectToPassiveAgent(String host, int port) throws IOException {
 205         if (host == null) {
 206             throw new NullPointerException();
 207         }
 208 
 209         for (int i = 0; ; i++) {
 210             try {
 211 //              return connect(new SocketConnection(host, port));
 212                 return connect(new InterruptableSocketConnection(host, port));
 213             }
 214             catch (ConnectException e) {
 215                 if (i == PASSIVE_AGENT_RETRY_LIMIT) {
 216                     throw e;
 217                 }
 218 
 219                 try {
 220                     Thread.currentThread().sleep(5000);
 221                 }
 222                 catch (InterruptedException ignore) {
 223                 }
 224             }
 225         }
 226     }
 227 
 228     private static final int PASSIVE_AGENT_RETRY_LIMIT = 12;
 229 
 230 
 231     //--------------------------------------------------------------------------
 232 
 233     /**
 234      * A Task provides the ability to do work remotely on an agent.
 235      */
 236     public class Task {
 237 
 238         /**
 239          * Create a connection to a agent retrieved from the agent pool.
 240          * @param c     The connection with which to communicate to the agent.
 241          */
 242         Task(Connection c) throws IOException {
 243             connection = c;
 244             in = new DataInputStream(new BufferedInputStream(c.getInputStream()));
 245             out = new DataOutputStream(new BufferedOutputStream(c.getOutputStream()));
 246         }
 247 
 248         /**
 249          * Get the connection being used for this task.
 250          * @return the connection to the remote agent.
 251          */
 252         public Connection getConnection() {
 253             return connection;
 254         }
 255 
 256         /**
 257          * Get the classpath to be used when loading classes on behalf of the
 258          * remote agent.
 259          * @return      An array of files and directories in which to look for classes to
 260          * be given to the remote agent.
 261          * @see #setClassPath(String)
 262          * @see #setClassPath(File[])
 263          */
 264         public File[] getClassPath() {
 265             return classPath;
 266         }
 267 
 268         /**
 269          * Set the classpath to be used for incoming requests for classes
 270          * from the remote agent.
 271          * @param path  The classpath to be set, using the <em>local</em>
 272          *              file and path separator characters.
 273          * @see #getClassPath
 274          * @see #setClassPath(File[])
 275          */
 276         public void setClassPath(String path) {
 277             classPath = split(path);
 278         }
 279 
 280 
 281         /**
 282          * Set the classpath to be used for incoming requests for classes
 283          * from the remote agent.
 284          * @param path  An array of files, representing the path to be used.
 285          * @see #getClassPath
 286          * @see #setClassPath(String)
 287          */
 288         public void setClassPath(File[] path) {
 289             if (path == null) {
 290                 throw new NullPointerException();
 291             }
 292 
 293             for (int i = 0; i < path.length; i++) {
 294                 if (path[i] == null) {
 295                     throw new IllegalArgumentException();
 296                 }
 297             }
 298 
 299             classPath = path;
 300         }
 301 
 302         /**
 303          * Set the shared classloader mode to be used for incoming requests for
 304          * classes from the remote agent.
 305          *
 306          * @param state The shared classloader mode, true to use a shared loader.
 307          */
 308         public void setSharedClassLoader(boolean state) {
 309             this.sharedCl = state;
 310         }
 311 
 312         /**
 313          * Set the timeout after command thread will be interrupted on the agent
 314          * @param timeout value in seconds
 315          */
 316         public void setAgentCommandTimeout(int timeout) {
 317             this.timeout = timeout;
 318         }
 319 
 320         /**
 321          * Request the agent for this client to execute a standard Test class.
 322          * @param tag   A string to identify the request in debug and trace output.
 323          * @param className The name of the class to execute. The class must be an
 324          *                      implementation of com.sun.javatest.Test, and must be
 325          *                      accessible by the agent: just the <em>name</em> of the class
 326          *                      is sent to the agent, not the classfile.
 327          * @param args  The arguments to be passed to the <code>run</code> method
 328          *                      of an instance of <code>classname</code> that will be executed
 329          *                      by the agent.
 330          * @param localizeArgs
 331          *                      Whether or not to instruct the agent to localize the args
 332          *                      to be passed to the test class. For example, this may be
 333          *                      necessary if the test has arguments that involve filenames
 334          *                      that differ from system to system.
 335          * @param log   A stream to which to write any data written to the log
 336          *                      stream when the test class is run.
 337          * @param ref   A stream to which to write any data written to the ref
 338          *                      stream when the test class is run.
 339          * @return              The status returned when the test class is run by the agent.
 340          * @see com.sun.javatest.Test
 341          */
 342         public Status executeTest(String tag, String className, String[] args,
 343                                   boolean localizeArgs,
 344                                   PrintWriter log, PrintWriter ref) {
 345             return run(tag, "executeTest", className, args, localizeArgs, log, ref);
 346         }
 347 
 348         /**
 349          * Request the agent for this client to execute a standard Command class.
 350          * @param tag   A string to identify the request in debug and trace output.
 351          * @param className The name of the class to execute. The class must be an
 352          *                      implementation of com.sun.javatest.Command, and must be
 353          *                      accessible by the agent: just the <em>name</em> of the class
 354          *                      is sent to the agent, not the classfile.
 355          * @param args  The arguments to be passed to the <code>run</code> method
 356          *                      of an instance of <code>classname</code> that will be executed
 357          *                      by the agent.
 358          * @param localizeArgs
 359          *                      Whether or not to instruct the agent to localize the args
 360          *                      to be passed to the test class. For example, this may be
 361          *                      necessary if the test has arguments that involve filenames
 362          *                      that differ from system to system.
 363          * @param log   A stream to which to write any data written to the log
 364          *                      stream when the command class is run.
 365          * @param ref   A stream to which to write any data written to the ref
 366          *                      stream when the command class is run.
 367          * @return              The status returned when the command class is run by the agent.
 368          * @see com.sun.javatest.Command
 369          */
 370         public Status executeCommand(String tag, String className, String[] args,
 371                                      boolean localizeArgs,
 372                                      PrintWriter log, PrintWriter ref) {
 373             return run(tag, "executeCommand", className, args, localizeArgs, log, ref);
 374         }
 375 
 376         /**
 377          * Request the agent for this client to execute the standard "main" method
 378          * for a class.
 379          * @param tag   A string to identify the request in debug and trace output.
 380          * @param className The name of the class to execute. The class must be an
 381          *                      implementation of com.sun.javatest.Command, and must be
 382          *                      accessible by the agent: just the <em>name</em> of the class
 383          *                      is sent to the agent, not the classfile.
 384          * @param args  The arguments to be passed to the <code>run</code> method
 385          *                      of an instance of <code>classname</code> that will be executed
 386          *                      by the agent.
 387          * @param localizeArgs
 388          *                      Whether or not to instruct the agent to localize the args
 389          *                      to be passed to the test class. For example, this may be
 390          *                      necessary if the test has arguments that involve filenames
 391          *                      that differ from system to system.
 392          * @param log   A stream to which to write any data written to the log
 393          *                      stream when the command class is run.
 394          * @param ref   A stream to which to write any data written to the ref
 395          *                      stream when the command class is run.
 396          * @return              Status.passed if the method terminated normally;
 397          *                      Status.failed if the method threw an exception;
 398          *                      Status.error if some other problem arose.
 399          * @see com.sun.javatest.Command
 400          */
 401         public Status executeMain(String tag, String className, String[] args,
 402                                      boolean localizeArgs,
 403                                      PrintWriter log, PrintWriter ref) {
 404             return run(tag, "executeMain", className, args, localizeArgs, log, ref);
 405         }
 406 
 407         private Status run(String tag, String request, String executable, String[] args,
 408                            boolean localizeArgs,
 409                            PrintWriter log, PrintWriter ref) {
 410             notifyStarted(connection, tag, request, executable, args, localizeArgs);
 411             Status result = null;
 412             try {
 413 //                boolean sharedClOption = false;
 414                 out.writeShort(Agent.protocolVersion);
 415                 out.writeUTF(tag);
 416                 out.writeUTF(request);
 417                 out.writeUTF(executable);
 418                 out.writeShort(args.length);
 419                 for (int i = 0; i < args.length; i++) {
 420                     out.writeUTF(args[i]);
 421 //                    if ("-sharedCl".equalsIgnoreCase(args[i]) ||
 422 //                        "-sharedClassLoader".equalsIgnoreCase(args[i])) {
 423 //                        sharedClOption = true;
 424 //                    }
 425                 }
 426 
 427                 out.writeBoolean(localizeArgs);
 428                 out.writeBoolean(classPath != null); // specify remoteClasses if classPath has been given
 429                 out.writeBoolean(sharedCl);
 430                 out.writeInt(timeout);
 431                 out.writeByte(0);
 432                 out.flush();
 433 
 434                 result = readResults(log, ref);
 435             }
 436             catch (IOException e) {
 437                 try {
 438                     if (out != null)
 439                         out.close();
 440                     if (in != null)
 441                         in.close();
 442                 }
 443                 catch (IOException ignore) {
 444                 }
 445                 if (e instanceof InterruptedIOException)
 446                     result = Status.error("Communication with agent interrupted! (timed out?)." +
 447                             "\n InterruptedException: " + e);
 448                 else
 449                     result = Status.error("Problem communicating with agent: " + e);
 450             }
 451             finally {
 452                 notifyFinished(connection, result);
 453             }
 454             return result;
 455         }
 456 
 457         private Status readResults(PrintWriter log, PrintWriter ref)
 458             throws IOException
 459         {
 460             Status status = null;
 461 
 462             while (status == null) {
 463                 int code = in.read();
 464                 switch (code) {
 465                 case -1: // unexpected EOF
 466                     status = Status.error("premature EOF from agent");
 467                     break;
 468 
 469                 case Agent.CLASS:
 470                     String className = in.readUTF();
 471                     //System.err.println("received request for " + className);
 472                     AgentRemoteClassData classData = locateClass(className);
 473                     classData.write(out);
 474                     out.flush();
 475                     break;
 476 
 477                 case Agent.DATA:
 478                     String resourceName = in.readUTF();
 479                     //System.err.println("received request for " + resourceName);
 480                     byte[] resourceData = locateData(resourceName);
 481                     if (resourceData == null)
 482                         //System.err.println("resource not found: " + className);
 483                         out.writeInt(-1);
 484                     else {
 485                         out.writeInt(resourceData.length);
 486                         out.write(resourceData, 0, resourceData.length);
 487                     }
 488                     out.flush();
 489                     //System.err.println("done request for " + resourceName);
 490                     break;
 491 
 492                 case Agent.STATUS:
 493                     int type = in.read();
 494                     String reason = in.readUTF();
 495                     switch (type) {
 496                     case Status.PASSED:
 497                         status = Status.passed(reason);
 498                         break;
 499                     case Status.FAILED:
 500                         status = Status.failed(reason);
 501                         break;
 502                     case Status.ERROR:
 503                         status = Status.error(reason);
 504                         break;
 505                     default:
 506                         status = Status.failed("Bad status from test: type=" + type + " reason=" + reason);
 507                         break;
 508                     }
 509                     break;
 510 
 511                 case Agent.LOG:
 512                     log.write(in.readUTF());
 513                     break;
 514 
 515                 case Agent.LOG_FLUSH:
 516                     log.write(in.readUTF());
 517                     log.flush();
 518                     break;
 519 
 520                 case Agent.REF:
 521                     ref.write(in.readUTF());
 522                     break;
 523 
 524                 case Agent.REF_FLUSH:
 525                     ref.write(in.readUTF());
 526                     ref.flush();
 527                     break;
 528                 }
 529             }
 530 
 531             out.close();
 532             in.close();
 533             connection.close();
 534 
 535             log.flush();
 536             ref.flush();
 537 
 538             // might be better not to flush these ...
 539             for (Enumeration e = zips.keys(); e.hasMoreElements(); ) {
 540                 File f = (File)(e.nextElement());
 541                 ZipFile z = zips.get(f);
 542                 zips.remove(f);
 543                 z.close();
 544             }
 545 
 546             return status;
 547         }
 548 
 549         private AgentRemoteClassData locateClass(String name) {
 550             //System.err.println("locateClass: " + name);
 551             if (classPath != null) {
 552                 String cname = name.replace('.', '/') + ".class";
 553                 for (int i = 0; i < classPath.length; i++) {
 554                     byte[] data;
 555                     if (classPath[i].isDirectory())
 556                         data = readFromDir(cname, classPath[i]);
 557                     else
 558                         data = readFromJar(cname, classPath[i]);
 559                     if (data != null) {
 560                         String codeSource = "";
 561                         try {
 562                             codeSource = classPath[i].toURI().toURL().getPath();
 563                         } catch (IOException e) {
 564                             //codeSource will not be set
 565                         }
 566                         AgentRemoteClassData classData = new AgentRemoteClassData(name, codeSource, data);
 567                         return classData;
 568                     }
 569                 }
 570             }
 571 
 572             return AgentRemoteClassData.NO_CLASS_DATA;
 573         }
 574 
 575         private byte[] locateData(String name) {
 576             //System.err.println("locateData: " + name);
 577             if (classPath != null) {
 578                 for (int i = 0; i < classPath.length; i++) {
 579                     byte[] data;
 580                     if (classPath[i].isDirectory())
 581                         data = readFromDir(name, classPath[i]);
 582                     else
 583                         data = readFromJar(name, classPath[i]);
 584                     if (data != null)
 585                         return data;
 586                 }
 587             }
 588 
 589             return null;
 590         }
 591 
 592         private byte[] readFromDir(String name, File dir) {
 593             //System.err.println("readFromDir: " + name + " " + dir);
 594             try {
 595                 File file = new File(dir, name);
 596                 return read(new FileInputStream(file), ((int) file.length()));
 597             }
 598             catch (IOException e) {
 599                 //System.err.println("readFromDir: " + e);
 600                 return null;
 601             }
 602         }
 603 
 604         private byte[] readFromJar(String name, File jarFile) {
 605             //System.err.println("readFromJar: " + name + " " + jarFile);
 606             try {
 607                 ZipFile z = zips.get(jarFile);
 608                 if (z == null) {
 609                     z = new ZipFile(jarFile);
 610                     zips.put(jarFile, z);
 611                 }
 612                 ZipEntry ze = z.getEntry(name);
 613                 if (ze == null)
 614                     return null;
 615                 return read(z.getInputStream(ze), ((int) ze.getSize()));
 616             }
 617             catch (IOException e) {
 618                 //System.err.println("readFromJar: " + e);
 619                 return null;
 620             }
 621         }
 622 
 623         private byte[] read(InputStream in, int size) throws IOException {
 624             //System.err.println("read: " + size);
 625             try {
 626                 byte data[] = new byte[size];
 627                 for (int total = 0; total < data.length; ) {
 628                     int n = in.read(data, total, data.length - total);
 629                     if (n > 0)
 630                         total += n;
 631                     else
 632                         throw new EOFException("unexpected end of file");
 633                 }
 634                 //System.err.println("read complete: " + data.length);
 635                 return data;
 636             }
 637             finally {
 638                 in.close();
 639             }
 640         }
 641 
 642         private File[] split(String s) {
 643             char pathCh = File.pathSeparatorChar;
 644             Vector<File> v = new Vector<>();
 645             int start = 0;
 646             for (int i = s.indexOf(pathCh); i != -1; i = s.indexOf(pathCh, start)) {
 647                 add(s.substring(start, i), v);
 648                 start = i + 1;
 649             }
 650             if (start != s.length())
 651                 add(s.substring(start), v);
 652             File[] path = new File[v.size()];
 653             v.copyInto(path);
 654             return path;
 655         }
 656 
 657         private void add(String s, Vector<File> v) {
 658             if (s.length() != 0)
 659                 v.addElement(new File(s));
 660         }
 661 
 662         private Connection connection;
 663         private DataInputStream in;
 664         private DataOutputStream out;
 665 
 666         private File[] classPath;
 667         private boolean sharedCl;
 668         private int timeout = 0;
 669         private Hashtable<File, ZipFile> zips = new Hashtable<>();
 670     }
 671 
 672 }
 673