1 /*
   2  * Copyright (c) 2007, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 package org.jemmy.input;
  24 
  25 
  26 import java.awt.EventQueue;
  27 import java.awt.image.BufferedImage;
  28 import java.io.BufferedReader;
  29 import java.io.File;
  30 import java.io.IOException;
  31 import java.io.InputStreamReader;
  32 import java.io.ObjectInputStream;
  33 import java.io.ObjectOutputStream;
  34 import java.io.OptionalDataException;
  35 import java.io.PrintWriter;
  36 import java.lang.reflect.Field;
  37 import java.lang.reflect.InvocationTargetException;
  38 import java.lang.reflect.Modifier;
  39 import java.net.ServerSocket;
  40 import java.net.Socket;
  41 import java.net.UnknownHostException;
  42 import java.util.Arrays;
  43 import java.util.HashSet;
  44 import java.util.Set;
  45 import java.util.logging.Level;
  46 import java.util.logging.Logger;
  47 import org.jemmy.JemmyException;
  48 import org.jemmy.Rectangle;
  49 import org.jemmy.env.Environment;
  50 import org.jemmy.env.Timeout;
  51 import org.jemmy.image.AWTImage;
  52 import org.jemmy.image.Image;
  53 import org.jemmy.image.PNGDecoder;
  54 import org.jemmy.image.PNGEncoder;
  55 import org.jemmy.timing.State;
  56 import org.jemmy.timing.Waiter;
  57 import org.jemmy.interfaces.Keyboard.KeyboardButton;
  58 import org.jemmy.interfaces.Mouse.MouseButton;
  59 
  60 /**
  61  * TODO: This code is inherently not thread safe
  62  * @author КАМ
  63  */
  64 class RobotExecutor {
  65 
  66     private static RobotExecutor instance;
  67     private AWTMap awtMap = null;
  68 
  69     public static RobotExecutor get() {
  70         if (instance == null) {
  71             instance = new RobotExecutor();
  72         }
  73         return instance;
  74     }
  75 
  76     /**
  77      * A reference to the robot instance.
  78      */
  79     protected ClassReference robotReference = null;
  80     protected Timeout autoDelay;
  81     private boolean inited = false;
  82     private boolean runInOtherJVM = false;
  83     private boolean ready = false;
  84     private boolean connectionEstablished = false;
  85     private ObjectOutputStream outputStream;
  86     private ObjectInputStream inputStream;
  87     private Socket socket;
  88     private int connectionPort;
  89     private String connectionHost;
  90     public static final int CONNECTION_TIMEOUT = Integer.parseInt(
  91             (String)Environment.getEnvironment().getProperty(
  92             AWTRobotInputFactory.OTHER_VM_CONNECTION_TIMEOUT_PROPERTY,
  93             Integer.toString(60000 * 15))); // 15 min
  94 
  95     public RobotExecutor() {
  96     }
  97 
  98     void setAWTMap(AWTMap awtMap) {
  99         this.awtMap = awtMap;
 100     }
 101 
 102     AWTMap getAWTMap() {
 103         if (awtMap == null) {
 104             awtMap = new AWTMap();
 105         }
 106         return awtMap;
 107     }
 108 
 109     private void ensureInited() {
 110         if (!inited) {
 111              runInOtherJVM = Boolean.parseBoolean((String)Environment.getEnvironment()
 112                      .getProperty(AWTRobotInputFactory.OTHER_VM_PROPERTY,
 113                      Boolean.toString(runInOtherJVM)));
 114              inited = true;
 115         }
 116     }
 117 
 118     public Image createScreenCapture(Rectangle screenRect) {
 119          Object result = makeAnOperation("createScreenCapture", new Object[] {
 120             new java.awt.Rectangle(screenRect.x, screenRect.y, screenRect.width,
 121                     screenRect.height) },
 122             new Class[] { java.awt.Rectangle.class });
 123          if (result.getClass().isAssignableFrom(BufferedImage.class)) {
 124              return new AWTImage(BufferedImage.class.cast(result));
 125          } else {
 126              throw new JemmyException("Screen capture (" + result
 127                      + ") is not a BufferedImage");
 128          }
 129     }
 130 
 131     public Object makeAnOperation(String method, Object[] params, Class[] paramClasses) {
 132         ensureInited();
 133         if (runInOtherJVM) {
 134             return makeAnOperationRemotely(method, params, paramClasses);
 135         } else {
 136             return makeAnOperationLocally(method, params, paramClasses);
 137         }
 138     }
 139 
 140     public void exit() {
 141         ensureInited();
 142         if (runInOtherJVM) {
 143             ensureConnection();
 144             try {
 145                 outputStream.writeObject("exit");
 146                 connectionEstablished = false;
 147                 deleteProperties();
 148             } catch (IOException ex) {
 149                 throw new JemmyException("Failed to invoke exit", ex);
 150             }
 151         }
 152     }
 153 
 154     private Object makeAnOperationLocally(String method, Object[] params, Class[] paramClasses) {
 155         if (robotReference == null) {
 156             initRobot();
 157         }
 158         try {
 159             convert(method, params, paramClasses);
 160             Object result = robotReference.invokeMethod(method, params, paramClasses);
 161             synchronizeRobot();
 162             return result;
 163         } catch (InvocationTargetException e) {
 164             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 165         } catch (IllegalStateException e) {
 166             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 167         } catch (NoSuchMethodException e) {
 168             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 169         } catch (IllegalAccessException e) {
 170             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 171         }
 172     }
 173 
 174     private int convert(Object obj) {
 175         if (MouseButton.class.isAssignableFrom(obj.getClass())) {
 176             return awtMap.convert((MouseButton)obj);
 177         } else if (KeyboardButton.class.isAssignableFrom(obj.getClass())) {
 178             return awtMap.convert((KeyboardButton)obj);
 179         } else {
 180             throw new JemmyException("Unable to recognize object", obj);
 181         }
 182     }
 183 
 184     private static final Set<String> convertables = new HashSet<String>(Arrays.asList(new String[] {"mousePress", "mouseRelease", "keyPress", "keyRelease"}));
 185 
 186     private void convert(String method, Object[] params, Class[] paramClasses) {
 187         if (convertables.contains(method))
 188             for (int i = 0; i < params.length; i++) {
 189             params[i] = new Integer(convert(params[i]));
 190             paramClasses[i] = Integer.TYPE;
 191         }
 192     }
 193 
 194     public static void main(String[] args) {
 195         System.setProperty("apple.awt.UIElement", "true");
 196         if (args.length != 0 && args.length != 1) {
 197             System.err.println("Usage: java ... [-D" +
 198                     Environment.JEMMY_PROPERTIES_FILE_PROPERTY + "=" +
 199                     "<.jemmy.properties full path>]" +
 200                     " RobotExecutor [connectionPort]");
 201             System.exit(-1);
 202         }
 203         if (args.length == 1) {
 204             Environment.getEnvironment().setProperty(
 205                     AWTRobotInputFactory.OTHER_VM_PORT_PROPERTY, args[0]);
 206         }
 207         RobotExecutor re = new RobotExecutor();
 208         try {
 209             re.server();
 210         } catch (Exception ex) {
 211             ex.printStackTrace(System.err);
 212             System.err.flush();
 213             System.exit(-1);
 214         }
 215     }
 216 
 217     private File props;
 218 
 219     private void deleteProperties() {
 220         if (props != null) {
 221             props.delete();
 222             props = null;
 223         }
 224     }
 225 
 226     private void prepareProperties() {
 227         deleteProperties();
 228         try {
 229             props = File.createTempFile(".jemmy.othervm.", ".properties");
 230             props.deleteOnExit();
 231             PrintWriter fw = new PrintWriter(props);
 232             for(Field f : AWTRobotInputFactory.class.getDeclaredFields()) {
 233                 if ((f.getModifiers() & Modifier.FINAL) != 0 &&
 234                         (f.getModifiers() & Modifier.STATIC) != 0 &&
 235                         f.getType().equals(String.class) &&
 236                         f.getName().startsWith("OTHER_VM_") &&
 237                         Environment.getEnvironment().getProperty((String)f.get(null)) != null) {
 238                     fw.println(f.get(null) + "=" + Environment.getEnvironment().getProperty((String)f.get(null)));
 239                 }
 240             }
 241             fw.close();
 242         } catch (IllegalArgumentException ex) {
 243             throw new JemmyException("Failed to create temporary properties file: " + props.getAbsolutePath(), ex);
 244         } catch (IllegalAccessException ex) {
 245             throw new JemmyException("Failed to create temporary properties file: " + props.getAbsolutePath(), ex);
 246         } catch (IOException ex) {
 247             throw new JemmyException("Failed to create temporary properties file: " + props.getAbsolutePath(), ex);
 248         }
 249 
 250     }
 251 
 252     private void startServer() {
 253         try {
 254             prepareProperties();
 255             ProcessBuilder pb = new ProcessBuilder("java",
 256                     //"-Xrunjdwp:transport=dt_socket,suspend=y,server=y,address=8000",
 257                     "-cp", System.getProperty("java.class.path"),
 258                     "-D" + Environment.JEMMY_PROPERTIES_FILE_PROPERTY +
 259                     "=" + props.getAbsolutePath(),
 260                     RobotExecutor.class.getName(),
 261                     Integer.toString(connectionPort));
 262             // TODO: Improve output
 263 //            System.out.println("Starting server");
 264 //            System.out.println("Command: " + pb.command());
 265 //            System.out.flush();
 266             pb.redirectErrorStream(true);
 267             final Process p = pb.start();
 268             new Thread() {
 269 
 270                 @Override
 271                 public void run() {
 272                     BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
 273                     while (true) {
 274                         try {
 275                             String line = br.readLine();
 276                             if (line == null) {
 277                                 break;
 278                             }
 279                             System.out.println("SERVER: " + line);
 280                         } catch (IOException ex) {
 281                             throw new JemmyException("Exception during other JVM output processing", ex);
 282                         }
 283                     }
 284                 }
 285             }.start();
 286         } catch (IOException ex) {
 287             throw new JemmyException("Failed to start other JVM", ex);
 288         }
 289     }
 290 
 291     public void ensureConnection() {
 292         ensureInited();
 293         if (runInOtherJVM && !connectionEstablished) {
 294             initClientConnection();
 295         }
 296     }
 297 
 298     private void initClientConnection() {
 299         connectionHost = (String)Environment.getEnvironment().getProperty(
 300              AWTRobotInputFactory.OTHER_VM_HOST_PROPERTY, "localhost");
 301         connectionPort = Integer.parseInt((String)Environment.getEnvironment()
 302              .getProperty(AWTRobotInputFactory.OTHER_VM_PORT_PROPERTY,
 303              "53669"));
 304         try {
 305             try {
 306                 socket = new Socket(connectionHost, connectionPort);
 307             } catch (IOException ex) {
 308                 if ("localhost".equalsIgnoreCase(connectionHost)
 309                         || "127.0.0.1".equals(connectionHost)) {
 310                     // TODO Improve check for localhost
 311                     startServer();
 312                     Environment.getEnvironment().getTimeout("");
 313                     Timeout waitTime = new Timeout("connection wait time", 5 * 60000);
 314                     socket = new Waiter(waitTime).ensureState(new State<Socket>() {
 315                         Exception ex;
 316                         public Socket reached() {
 317                             Socket socket = null;
 318                             try {
 319                                 socket = new Socket(connectionHost, connectionPort);
 320                             } catch (UnknownHostException ex1) {
 321                                 ex = ex1;
 322                             } catch (Exception ex1) {
 323                                 ex = ex1;
 324                             }
 325                             return socket;
 326                         }
 327 
 328                         @Override
 329                         public String toString() {
 330                             if (ex != null) {
 331                                 // TODO: Provide better mechanics for exception handling
 332                                 Logger.getLogger(RobotExecutor.class.getName())
 333                                         .log(Level.INFO, null, ex);
 334                             }
 335                             return "Waiting for connection to be established " +
 336                                     "with other JVM (" + connectionHost
 337                                     + ":" + connectionPort + ", exception: " + ex + ")";
 338                         }
 339                     });
 340                 } else {
 341                     throw new JemmyException("Failed to establish socket " +
 342                             "connection with other JVM (" + connectionHost
 343                             + ":" + connectionPort + ")", ex);
 344                 }
 345             }
 346             outputStream = new ObjectOutputStream(socket.getOutputStream());
 347             inputStream = new ObjectInputStream(socket.getInputStream());
 348 
 349             connectionEstablished = true;
 350             ready = true;
 351 
 352             System.out.println("Connection established!");
 353             setAutoDelay(autoDelay);
 354         } catch (IOException ex) {
 355             throw new JemmyException("Failed to establish socket connection " +
 356                     "with other JVM (" + connectionHost + ":" + connectionPort
 357                     + ")", ex);
 358         }
 359     }
 360 
 361     public synchronized Object getProperty(String name) {
 362         ensureConnection();
 363         try {
 364             outputStream.writeObject("getProperty");
 365             outputStream.writeObject(name);
 366             Object result = inputStream.readObject();
 367             String response = (String)(inputStream.readObject());
 368             if (!"OK".equals(response)) {
 369                 throw new JemmyException("Remote operation didn't succeed");
 370             }
 371             return result;
 372         } catch (ClassNotFoundException ex) {
 373             throw new JemmyException("Socket communication with other JVM failed", ex);
 374         } catch (OptionalDataException ex) {
 375             throw new JemmyException("Socket communication with other JVM " +
 376                     "failed: OptionalDataException eof = " + ex.eof + ", " +
 377                     "length = " + ex.length, ex);
 378         } catch (IOException ex) {
 379             throw new JemmyException("Socket communication with other JVM failed", ex);
 380         }
 381     }
 382 
 383     private synchronized Object makeAnOperationRemotely(String method, Object[] params, Class[] paramClasses) {
 384         ensureConnection();
 385         try {
 386             outputStream.writeObject("makeAnOperation");
 387             outputStream.writeObject(method);
 388             outputStream.writeObject(params);
 389             outputStream.writeObject(paramClasses);
 390             Object result;
 391             String response = (String)(inputStream.readObject());
 392             if ("image".equals(response)) {
 393                 result = PNGDecoder.decode(inputStream, false);
 394             } else {
 395                 if (!"OK".equals(response)) {
 396                     throw new JemmyException("Remote operation didn't succeed");
 397                 }
 398                 result = inputStream.readObject();
 399             }
 400             return result;
 401         } catch (ClassNotFoundException ex) {
 402             throw new JemmyException("Socket communication with other JVM failed", ex);
 403         } catch (OptionalDataException ex) {
 404             throw new JemmyException("Socket communication with other JVM " +
 405                     "failed: OptionalDataException eof = " + ex.eof + ", " +
 406                     "length = " + ex.length, ex);
 407         } catch (IOException ex) {
 408             throw new JemmyException("Socket communication with other JVM failed", ex);
 409         }
 410     }
 411 
 412     private void server() {
 413         System.out.println("Robot ready!");
 414         System.out.flush();
 415         ServerSocket sc;
 416         connectionPort = Integer.parseInt((String)Environment.getEnvironment()
 417              .getProperty(AWTRobotInputFactory.OTHER_VM_PORT_PROPERTY,
 418              "53669"));
 419         while(true) {
 420             Thread watchdog = new Thread("RobotExecutor.server watchdog") {
 421 
 422                 @Override
 423                 public void run() {
 424                     try {
 425                         Thread.sleep(CONNECTION_TIMEOUT);
 426                         System.out.println("Exiting server as there is no " +
 427                                 "connection for " + CONNECTION_TIMEOUT / 60000.0
 428                                 + " minutes");
 429                         System.out.flush();
 430                         System.exit(0);
 431                     } catch (InterruptedException ex) {
 432                         // Ignoring exception as it is okay
 433                     }
 434                 }
 435 
 436             };
 437             watchdog.start();
 438             System.out.println("Waiting for incoming connection for up to "
 439                     + CONNECTION_TIMEOUT / 60000.0 + " minutes");
 440             try {
 441                 sc = new ServerSocket(connectionPort);
 442                 socket = sc.accept();
 443                 watchdog.interrupt();
 444             } catch (IOException ex) {
 445                 throw new JemmyException("Can't establish connection with client", ex);
 446             }
 447             System.out.println("Connection established!");
 448             try {
 449                 inputStream = new ObjectInputStream(socket.getInputStream());
 450                 outputStream = new ObjectOutputStream(socket.getOutputStream());
 451                 while(true) {
 452                     String command = (String)inputStream.readObject();
 453                     if ("exit".equals(command)) {
 454                         System.exit(0);
 455                     }
 456                     if ("getProperty".equals(command)) {
 457                         String property = (String)inputStream.readObject();
 458                         outputStream.writeObject(Environment.getEnvironment().getProperty(property));
 459                         outputStream.writeObject("OK");
 460                     }
 461                     if ("makeAnOperation".equals(command)) {
 462                         String method = (String)inputStream.readObject();
 463                         Object[] params = (Object[])inputStream.readObject();
 464                         Class[] paramClasses = (Class[])inputStream.readObject();
 465                         Object result = makeAnOperationLocally(method, params,
 466                                 paramClasses);
 467                         if (result instanceof BufferedImage) {
 468                             outputStream.writeObject("image");
 469                             BufferedImage image = BufferedImage.class.cast(result);
 470                             new PNGEncoder(outputStream, PNGEncoder.COLOR_MODE)
 471                                     .encode(image, false);
 472                         } else {
 473                             outputStream.writeObject("OK");
 474                             outputStream.writeObject(result);
 475                         }
 476                     }
 477                 }
 478             } catch (ClassNotFoundException ex) {
 479                 throw new JemmyException("Socket communication with other " +
 480                         "JVM failed", ex);
 481             } catch (IOException ex) {
 482                 Logger.getLogger(RobotExecutor.class.getName())
 483                         .log(Level.SEVERE, null, ex);
 484             } finally {
 485                 if (socket != null) {
 486                     try {
 487                         socket.close();
 488                     } catch (IOException ex) {
 489                         Logger.getLogger(RobotExecutor.class.getName()).log(
 490                                 Level.SEVERE, "Exception during socket closing", ex);
 491                     }
 492                 }
 493                 if (sc != null) {
 494                     try {
 495                         sc.close();
 496                     } catch (IOException ex) {
 497                         Logger.getLogger(RobotExecutor.class.getName()).log(
 498                                 Level.SEVERE, "Exception during server socket " +
 499                                 "closing", ex);
 500                     }
 501                 }
 502             }
 503         }
 504     }
 505 
 506     private void initRobot() {
 507         // need to init Robot in dispatch thread because it hangs on Linux
 508         // (see http://www.netbeans.org/issues/show_bug.cgi?id=37476)
 509         if (EventQueue.isDispatchThread()) {
 510             doInitRobot();
 511         } else {
 512             try {
 513                 EventQueue.invokeAndWait(new Runnable() {
 514 
 515                     public void run() {
 516                         doInitRobot();
 517                     }
 518                 });
 519             } catch (InterruptedException ex) {
 520                 throw new JemmyException("Failed to initialize robot", ex);
 521             } catch (InvocationTargetException ex) {
 522                 throw new JemmyException("Failed to initialize robot", ex);
 523             }
 524         }
 525     }
 526 
 527     private void doInitRobot() {
 528         try {
 529             ClassReference robotClassReverence = new ClassReference("java.awt.Robot");
 530             robotReference = new ClassReference(robotClassReverence.newInstance(null, null));
 531             if (awtMap == null) {
 532                 awtMap = new AWTMap();
 533             }
 534             setAutoDelay(autoDelay);
 535             ready = true;
 536         } catch (InvocationTargetException e) {
 537             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 538         } catch (IllegalStateException e) {
 539             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 540         } catch (NoSuchMethodException e) {
 541             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 542         } catch (IllegalAccessException e) {
 543             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 544         } catch (ClassNotFoundException e) {
 545             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 546         } catch (InstantiationException e) {
 547             throw (new JemmyException("Exception during java.awt.Robot accessing", e));
 548         }
 549     }
 550 
 551     /**
 552      * Calls <code>java.awt.Robot.waitForIdle()</code> method.
 553      */
 554     protected void synchronizeRobot() {
 555         ensureInited();
 556         if (!runInOtherJVM) {
 557             // TODO: It looks like this method is rudimentary
 558             if (!EventQueue.isDispatchThread()) {
 559                 if (robotReference == null) {
 560                     initRobot();
 561                 }
 562                 try {
 563                     robotReference.invokeMethod("waitForIdle", null, null);
 564                 } catch (Exception e) {
 565                     e.printStackTrace();
 566                 }
 567             }
 568         }
 569     }
 570 
 571     public void setAutoDelay(Timeout autoDelay) {
 572         this.autoDelay = autoDelay;
 573         if (ready) {
 574             makeAnOperation("setAutoDelay", new Object[]{new Integer((int) ((autoDelay != null) ? autoDelay.getValue() : 0))}, new Class[]{Integer.TYPE});
 575         }
 576     }
 577 
 578     public boolean isRunInOtherJVM() {
 579         ensureInited();
 580         return runInOtherJVM;
 581     }
 582 
 583     public void setRunInOtherJVM(boolean runInOtherJVM) {
 584         if (inited && this.runInOtherJVM && this.connectionEstablished && !runInOtherJVM) {
 585             shutdownConnection();
 586         }
 587         this.runInOtherJVM = runInOtherJVM;
 588         inited = true;
 589         ready = false;
 590     }
 591 
 592     private void shutdownConnection() {
 593         try {
 594             outputStream.writeObject("exit");
 595             socket.close();
 596             connectionEstablished = false;
 597         } catch (IOException ex) {
 598             throw new JemmyException("Failed to shutdown connection", ex);
 599         }
 600     }
 601 }