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 }