1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 | package Torello.Browser; import java.util.*; import javax.json.*; import java.io.*; import java.net.URL; import java.util.function.Function; import java.util.function.Consumer; import Torello.Java.*; import Torello.Java.Additional.*; import static Torello.Java.C.*; import Torello.HTML.Scrape; import NeoVisionaries.WebSockets.WebSocketException; /** * This class helps to start an headless-instance of a Web-Browser, and open a connection to * that browser. * * <EMBED CLASS='external-html' DATA-FILE-ID=BRDPC> */ public class BRDPC { private BRDPC() { } // ******************************************************************************************** // ******************************************************************************************** // Looking up the name of all registered events // ******************************************************************************************** // ******************************************************************************************** // Converts a Browser-Event name to the **PARTIALLY-QUALIFIED** Event-Name @SuppressWarnings("unchecked") private static final TreeMap<String, String> eventNames = (TreeMap<String, String>) LFEC.readObjectFromFile_JAR (BRDPC.class, "data-files/EventNames.tmdat", true, TreeMap.class); /* * Chrome only provides the name of the event, this will return the partially qualified name * of the class - <B STYLE='color: red;'><I>including the containing / parent class name, * which is needed to construct an instance of the event-class</I></B>. * * <BR /><BR /><B>NOTE:</B> The package name <B>({@code Torello.Browser})</B> is not included * in the returned {@code String}. */ static final String getEventClassName(String eventName) { return eventNames.get(eventName); } // ******************************************************************************************** // ******************************************************************************************** // // ******************************************************************************************** // ******************************************************************************************** /** A singleton instance of the web-socket connection. */ public static WebSocketSender defaultSender = null; // A lambda used by many of the types in this package. static final Function<JsonObject, Ret0> NoReturnValues = (JsonObject jo) -> Ret0.R0; /** * A commmon location for the Chrome-Binary on a Windows Machine. * * <BR /><BR /><B STYLE='color: red;'>IMPORTANT:</B> When this class' * {@link #startBrowserExecutable(boolean, Integer, String, int)} method is invoked, it * will attempt to run the program specified by this field. <B><I>It is of utmost importance * to set this field to whatever location has your Chrome-Binary!</I></B> */ public static String CHROME_EXE = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"; /** * A common location for the Chome-Canary Binary on a Windows Machine. * * <BR /><BR /><B STYLE='color: red;'>IMPORANT:</B> I don't actually know when or why one * would use the <B>Canary</B>-Binary, rather than the standard <B>Chrome</B> executable. * I have read that the original intent of this particular application (which is a full * fledged instance of <B>Chrome</B>) - was to provide a headless version of the standard * Google-Browser. * * <BR /><BR />The current version of a <B>Chrome</B>-Executable seems to accept * {@code Remote Debug Port} commands quite readily, without having to use <B>Canary</B>. * I will leave it here as a discussion-point. */ public static String CANARY_EXE = "C:\\Users\\ralph\\AppData\\Local\\Google\\Chrome SxS\\Application\\Chrome"; /** * Configuration-flag for setting the verbosity of this library-package. * * <BR /><BR />To sent more vebose messages to your {@code StorageWriter} instance (field * {@link #sw}), change the value of this field to {@code TRUE}. */ public static boolean QUIET = false; /** * The log output. Output text will be sent to this field. This field is not declared * {@code final}, and (obviously) may be set to any desired log output. This field is not * exactly thread-safe - <I>it is shared by all threads which use the headless browser * library tools!</I> * * <BR /><BR />Because web-sockets, themselves are a multi-threaded application, there isn't * really a way to identify which thread has caused a particular response. Some percentage of * the messages received from the browser will be events, and in a multi-threaded application * of this tool, there would be no way to identify which thread caused which event. */ public static StorageWriter sw = new StorageWriter(); // ******************************************************************************************** // ******************************************************************************************** // Starting the Browser, from Java rather than the command-line // ******************************************************************************************** // ******************************************************************************************** /** * <H3 CLASS=JDBanner>A browser-instance must be loaded before using this package's commands * </H3> * * <BR /> * Starts a headless (or full/complete) browser instance. All this method does is use the * Standard-Java {@code java.lang.Process} class to start an operating-system executable. This * just means invoking the executable specified by {@link #CHROME_EXE}. * * @param headless If {@code FALSE} is passed to this method, then the option {@code -headless} * will not be passed to the browser at startup. In such scenarios, an actual browser should * popup on your desktop. Normally, this parameter should receive {@code TRUE}. * * @param port It is standard operating procedure to pass {@code 9222} to this parameter. * {@code 9223} is also very common. You may pass null to this parameter, and the default * value will be used (which is {@code 9223}). * * @param initialURL You may elect to have the page open at the specified {@code URL}. This * parameter may be null, and when it is, it shall be ignored. * * @param maxCount Some delay is inserted between the starting of a browser executable, and * this method exiting. * * @throws IOException If there are any web-socket or http problems when attempting to connect * to the browser. */ public static void startBrowserExecutable (boolean headless, Integer port, String initialURL, int maxCount) throws IOException { if (port == null) port = 9223; if (maxCount < 2) maxCount = 2; Vector<String> comm = new Vector<>(); comm.add(CHROME_EXE); if (! headless) comm.add("-disable-gpu"); comm.add("-remote-debugging-port=" + port); if (headless) comm.add("-headless"); // I don't know as much about Google-Chrome as you might think... // I don't know what this does... // comm.add("-no-sandbox"); if (initialURL != null) comm.add(initialURL); String[] COMM = comm.toArray(new String[0]); for (String s : COMM) System.out.print(s + " "); System.out.println(); Process pro = java.lang.Runtime.getRuntime().exec(COMM); R reader1 = new R(pro.getInputStream()); R reader2 = new R(pro.getErrorStream()); Thread t1 = new Thread(reader1); Thread t2 = new Thread(reader2); t1.setDaemon(true); t2.setDaemon(true); t1.start(); t2.start(); int i = 0; System.out.println("Waiting to read the code."); int counter = 0; while (reader1.stillReading && reader2.stillReading) if (i++ % 500000000 == 0) { System.out.print("."); if (++counter == maxCount) break; } } private static class R implements Runnable { private BufferedReader br; public boolean stillReading = true; public R(InputStream is) { this.br = new BufferedReader(new InputStreamReader(is)); } public void run() { try { String s = null; long time = System.currentTimeMillis(); while ((s = br.readLine()) != null) { System.out.println(s); long current = System.currentTimeMillis(); if ((time - current) > 4000) break; time = current; } stillReading = false; } catch (IOException ioe) { System.out.println(EXCC.toString(ioe)); System.exit(0); } } } // ******************************************************************************************** // ******************************************************************************************** // Retrieving Browser Information // ******************************************************************************************** // ******************************************************************************************** /** * You may use this method (if you so choose, for whatever reason) to retrieve the Remote * Debugging Port API from Google. This method will return a (very long) JSON-File, as a * {@code java.lang.String} * * @param port This should be the port that was used to start the server. This is, by default, * port {@code 9223}. * * @return A very-long JSON-{@code String} containing the currently-exported API that your * web-browser provides. * * @throws IOException If there are problems sending this request, or retrieving the response, * this throws. */ public static String readAPIFromServer(Integer port) throws IOException { if (port == null) port = 9223; String urlStr = "http://127.0.0.1:" + port + "/json/protocol"; URL url = new URL(urlStr); System.out.println("Querying WS Server for JSON\n" + url); // NOTE: This is a very large JSON File - more than 20,000 lines of text return Scrape.scrapePage(url); } /** * This is supposed to be the easy part. There is a very long "Identifier String" that * contains some connection information. * * <BR /><BR /><B STYLE='color: red;'>NOTE:</B> There is really no need to use this method, * as this "Connection Stuff" is all taken care of inside this class (and the class * {@link WebSocketSender}). * * <BR /><BR />If, for some reason, there is a need to sue the feature of having multiple * browser connections, and it is deemed important to build your own instance of * {@link WebSocketSender} - <I>you will have to retrieve this {@code URL} information in order * to construct a {@link WebSocketSender} instance.</I> * * @param port The browser-port upon. which the {@code WebSocket} was connected * * @return An instance of {@link Ret2}: * * <BR /><BR /><UL CLASS=JDUL> * <LI><B>{@code String}</B> (Browser {@code URL}) * <BR /><BR />The complete {@code URL} to use when connecting to the browser over its * RDP Connection. * <BR /><BR /> * </LI> * <LI><B>{@code String}</B> (Browser {@code URL}) * <BR /><BR />A "Connection Code" that the browser uses to identify instances of a * Web-Socket Connection. * <BR /><BR /> * </LI> * </UL> * * @throws IOException When connecting to the browser, this is always a possibility. */ public static Ret2<String, String> readURLToUseFromServer(Integer port) throws IOException { if (port == null) port = 9223; // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** // Retrieve the Web-Socket Address from URL - Attemt #1 // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** URL url = new URL("http://127.0.0.1:" + port + "/json/list"); if (! QUIET) sw.println( '\n' + BRED + "ATTEMPT #1:" + RESET + " (Works when there is already a page opened)\n" + "Querying WebSocketServer for Listening Address Using:\n\t" + BYELLOW + url.toString() + RESET ); try { String json = Scrape.scrapePage(url); StringReader sr = new StringReader(json); if (! QUIET) sw.println("Attempt #1, Server Responded With:\n" + json); if (json.length() > 10) // else it gave us an empty response { JsonArray jArr = Json.createReader(sr).readArray(); JsonObject jObj = null; // used in next-section if ((jArr != null) && (jArr.size() > 0)) { String res = jArr.getJsonObject(0).getString("webSocketDebuggerUrl", null); String code = StringParse.fromLastFrontSlashPos(res); if (res != null) return new Ret2<>(res, code); } } } catch (Exception e) { sw.println( BGREEN + "NOTE: FOR ATTEMPT #1, THIS IS SOMEWHAT EXPECTED" + RESET + '\n' + BRED + "Exception Message Attempt #1:\n\t" + RESET + e.getMessage() + '\n' + "-------------------------------------------------------------------\n" + EXCC.toString(e) + "-------------------------------------------------------------------\n" ); } // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** // Retrieve the Web-Socket Address from URL #2 // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** url = new URL("http://127.0.0.1:" + port + "/json/version"); // Again, I'm not 100% what chrome is really doing here... // url = new URL("http://127.0.0.1/json/version"); if (! QUIET) sw.println( BRED + "ATTEMPT #2:" + RESET + " (Creates an entirely new connection)\n" + "Querying WebSocketServer for Listening Address Using:\n\t" + BYELLOW + url.toString() + RESET ); try { String json = Scrape.scrapePage(url); StringReader sr = new StringReader(json); JsonObject jObj = Json.createReader(sr).readObject(); if (! QUIET) sw.println("Attempt #2, Server Responded With:\n" + json); if (jObj != null) { String res = jObj.getString("webSocketDebuggerUrl", null); String code = StringParse.fromLastFrontSlashPos(res); sw.println( "res: " + res + '\n' + "code: " + code ); if ((res != null) && res.startsWith("ws://")) return new Ret2<>(res, code); } } catch (Exception e) { sw.println( BRED + "Exception Message Attempt #2:\n\t" + RESET + e.getMessage() + '\n' + "-------------------------------------------------------------------\n" + EXCC.toString(e) + "-------------------------------------------------------------------\n" ); } // Failed sw.println(BRED + "Unable to retrieve Chrome Web-Socket URL. Sorry buddy." + RESET); return null; } /** * After an instance of Chrome has been started, this method should be used to create a * <B>{@code WebSocket}</B>-Connection to the browser. * * <BR /><BR /><H2 CLASS=JDBanner>You must invoked this method before sending commands</H2> * * <I CLASS=JDBanner>After starting a browser instance using the method * {@link #startBrowserExecutable(boolean, Integer, String, int)}, you will also have to open * a Web-Socket Connection to the browser using this method. Then (and only then), is it * possible to begin sending requets to the browser.</I> * * @param port You should just elect to use {@code 9223}, that is what is recommended by all of * the sources on that Internet that explain this browser-feature. Chrome will be listening on * whatever port you started the headless version with. * * <BR /><BR /><B>NOTE:</B> You don't have to start the browser in headless mode. Controlling * a full-fleged, and opened, browser is allowed with Chrome. * * @param quiet You may configure the verbosity-level for the * <B>{@code Websocket}</B>-Connection to be different from the verbosity-level for this class * (class {@code BRDPC}). * * @param eventHandler It is advisable to register an event-handler. This parameter may be * passed null, and if it is, events will not be handled (they are then ignored). * * @throws IOException Connecting to the browser may cause this exception. * * @throws WebSocketException The {@code NeoVisionaries} package for {@code WebSockets} may * throw this when attempting to build the connection. */ public static boolean buildDefaultWebSocketConnection (Integer port, boolean quiet, Consumer<BrowserEvent> eventHandler) throws IOException, WebSocketException { Ret2<String, String> browserListenerURL = readURLToUseFromServer(port); if ((browserListenerURL == null) || (browserListenerURL.a == null)) { sw.println("Unable to retrieve a Web-Socket Connection. No Request URL's"); return false; } BRDPC.defaultSender = new WebSocketSender( browserListenerURL.a, quiet, (eventHandler == null) ? null : (Object o) -> eventHandler.accept((BrowserEvent) o) ); if (! QUIET) sw.println ("Web Socket Connection Opened:\n" + BYELLOW + browserListenerURL.a + RESET); return true; } // ******************************************************************************************** // ******************************************************************************************** // Exception throw methdods used by all of the Library-Classes // ******************************************************************************************** // ******************************************************************************************** /** Configuration to Override the null-check. */ public static final boolean ALLOW_NULLABLE_PARAMETERS = false; /** * In order to differentiate between <B>{@code 'optional'}</B> (and required) parameters and * fields, quite an amount of effort was invested in checking that required variables are * not allowed to be null. * * <BR /><BR />This method ensures a consistent-looking error message whenever there is a * null parameter passed that should not have been null. */ static void throwNPE(String requiredPropertyParameterName) { if (ALLOW_NULLABLE_PARAMETERS) return; throw new NullPointerException( "The parameter '" + requiredPropertyParameterName + "' is not marked is optional. " + "This means that null may not be passed to this parameter in this method. However " + "this parameter received a null value." ); } /** * In addition to null checks, a lot of the Browser's API provides its own version of the * Java {@code 'enum'} by listing valid-{@code String's} (as pre-defined lists) that an * {@code String} parameter or field will accept. * * <BR /><BR />If a programmer ever provides a {@code String} to a {@code String} parameter * or field for which an enumerated-list of valid-values of the parameter or field has been * provided, <I>but that value doesn't meet the enumerated-list's requiremets</I>, then an * {@code IllegalArgumentException} will throw. * * @param parameterName The name of the offending parameter. * @param parameterValue The {@code String}-Value of the offending parameter. * * @throws IllegalArgumentException This method shall this exception, everytime. */ static void throwIAE(String parameterName, String parameterValue, String... enumStrArr) { // NOTE: This presumes that 'non-optional' parameters have already been null-checked if (parameterValue == null) return; if (StrCmpr.equalsNAND(parameterValue, enumStrArr)) throw new IllegalArgumentException( "The parameter '" + parameterName + "' is a String parameter whose values are " + "limited to the Enumerated Strings listed in this method's documentation page. " + "Unfortunately, this method received [" + parameterValue + "] as a String, " + "which is not among the listed values for this String Parameter." ); } /** * Also throws exception for enumerated strings */ static void checkIAE (String parameterName, String parameterValue, String enumName, String... enumStrArr) { // NOTE: This presumes that 'non-optional' parameters have already been null-checked if (parameterValue == null) return; if (StrCmpr.equalsNAND(parameterValue, enumStrArr)) throw new IllegalArgumentException( "The parameter '" + parameterName + "' is a String parameter whose values are " + "limited to the Enumerated Strings listed in String-Array[] '" + enumName + "'. " + "Unfortunately, this method received [" + parameterValue + "] as a String, " + "and that is not among the listed values for the String Parameter." ); } } |