├── .gitignore ├── .idea ├── inspectionProfiles │ └── Project_Default.xml ├── libraries │ └── org_java_websocket_Java_WebSocket_1_3_7.xml ├── markdown-exported-files.xml ├── markdown-history.xml ├── markdown-navigator.xml ├── markdown-navigator │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── .travis.yml ├── JavaFx-WebView-Debugger.iml ├── LICENSE ├── META-INF └── MANIFEST.MF ├── README.md ├── VERSION.md ├── assets ├── RoughLoadSequence.png └── RoughLoadSequence.scap ├── images └── DevTools.png ├── pom.xml └── src └── main ├── java └── com │ └── vladsch │ └── javafx │ └── webview │ └── debugger │ ├── DevToolsDebugProxy.java │ ├── DevToolsDebuggerJsBridge.java │ ├── DevToolsDebuggerServer.java │ ├── JfxConsoleApiArgs.java │ ├── JfxDebugProxyJsBridge.java │ ├── JfxDebugProxyJsBridgeDelegate.java │ ├── JfxDebuggerAccess.java │ ├── JfxDebuggerConnector.java │ ├── JfxDebuggerProxy.java │ ├── JfxScriptArgAccessor.java │ ├── JfxScriptArgAccessorDelegate.java │ ├── JfxScriptStateProvider.java │ ├── JfxWebSocketServer.java │ └── LogHandler.java ├── javadoc ├── overview.html └── overview.md └── resources ├── console-test.js └── markdown-navigator.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | /lib/ 3 | /target/ 4 | /.idea/workspace.xml 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 105 | -------------------------------------------------------------------------------- /.idea/libraries/org_java_websocket_Java_WebSocket_1_3_7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/markdown-exported-files.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/markdown-history.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | code { 73 | background-color: rgba(255, 0, 102, 0.06); 74 | color: #b00042; 75 | padding-top: 4px; 76 | padding-bottom: 4px; 77 | } 78 | 79 | .search-highlight { 80 | border: solid 1px #000; 81 | padding: 1px; 82 | background-color: #ffff00; 83 | color: #000; 84 | } 85 | 86 | .selection-highlight { 87 | color:#000; 88 | padding:1px 0 1px 0; 89 | border: solid 1px #52CC7A; 90 | background-color: #B5FFCE; 91 | } 92 | 93 | .red { 94 | color:red; 95 | } 96 | 97 | .blue { 98 | color:blue; 99 | } 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | dist: precise 3 | sudo: required 4 | install: true 5 | 6 | jdk: 7 | - oraclejdk8 8 | 9 | env: 10 | - TEST=java 11 | 12 | script: 13 | - 'if [[ $TEST = java ]]; then mvn test -Dsurefire.useFile=false; fi' 14 | 15 | -------------------------------------------------------------------------------- /JavaFx-WebView-Debugger.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vladimir Schneider 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Class-Path: annotations-15.0.jar javax.json-1.0.4.jar Java-WebSocket-1.3.7.jar log4j-1.2.17.jar boxed-json.jar 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaFX WebView Debugger 2 | 3 | ##### Via WebSocket connection to Google Chrome Dev Tools 4 | 5 | [![Build status](https://travis-ci.org/vsch/Javafx-WebView-Debugger.svg?branch=master)](https://travis-ci.org/vsch/Javafx-WebView-Debugger) 6 | [![Maven Central status](https://img.shields.io/maven-central/v/com.vladsch.javafx-webview-debugger/javafx-webview-debugger.svg)](https://search.maven.org/search?q=g:com.vladsch.javafx-webview-debugger) 7 | 8 | JavaFx WebView debugging with Chrome Dev tools is highly dependent on Google Chrome version and 9 | JavaFx Version. 10 | 11 | If you can debug your scripts using another environment, I would highly recommend doing that instead. 12 | Only use JavaFx WebView based debugging when you absolutely need to debug the code under this 13 | environment. 14 | 15 | JavaFx I use for debugging is JetBrains OpenJDK 1.8.0u152 build 1136 which I found to be the 16 | most stable for JavaFx WebView debugging. Other versions are more quirky and after starting 17 | debug server they need a page reload from the context menu before connecting Chrome Dev Tools. 18 | Otherwise, JavaFX crashes and takes the application with it. 19 | 20 | I use Chrome version 65.0.3325.181 under OS X. Later versions work but console has no output. I 21 | have not investigated what is causing this. You can download older Chrome versions from 22 | **[Google Chrome Older Versions Download (Windows, Linux & Mac)](https://www.slimjet.com/chrome/google-chrome-old-version.php)** 23 | 24 | Here is a teaser screenshot of dev tools running with JavaFX WebView, showing off the console 25 | logging from scripts, with caller location for one click navigation to source: 26 | 27 | ![DevTools](images/DevTools.png) 28 | 29 | ### Requirements 30 | 31 | * Java 8 (not tested with 9) 32 | * The project is on Maven: `com.vladsch.javafx-webview-debugger` 33 | * dependencies: 34 | * `org.glassfish:javax.json` 35 | * `org.jetbrains.annotations` 36 | * `com.vladsch.boxed-json` 37 | * `org.apache.log4j` 38 | 39 | ### Quick Start 40 | 41 | Take a look at the [Web View Debug Sample] application for a working example. You can play with 42 | it and debug the embedded scripts before deciding whether you want to instrument your own 43 | application for Chrome Dev Tools debugging. 44 | 45 | For Maven: 46 | 47 | ```xml 48 | 49 | com.vladsch.javafx-webview-debugger 50 | javafx-webview-debugger 51 | 0.7.6 52 | 53 | ``` 54 | 55 | ### Credit where credit is due 56 | 57 | I found out about the possibility of having any debugger for JavaFX WebView in 58 | [mohamnag/javafx_webview_debugger]. That implementation in turn was based on the solution found 59 | by Bosko Popovic. 60 | 61 | I am grateful to Bosko Popovic for discovering the availability and to Mohammad Naghavi creating 62 | an implementation of the solution. Without their original effort this solution would not be 63 | possible. 64 | 65 | ### Finally real debugging for JavaFX WebView 66 | 67 | Working with JavaScript code in WebView was a debugging nightmare that I tried to avoid. My 68 | excitement about having a real debugger diminished when I saw how little was available with a 69 | bare-bones implementation due to WebView's lack of a full Dev Tools protocol. The console did 70 | not work and none of the console api calls from scripts made it to the Dev Tools console. No 71 | commandLineAPI for the same reason. Having to use logs for these is last century. 72 | 73 | A proxy to get between Chrome Dev Tools and WebView solved most of the WebView debugging 74 | protocol limitations. It also allowed to prevent conditions that caused WebView to crash and 75 | bring the rest of the application with it. I don't mean a Java exception. I mean a core dump and 76 | sudden exit of the application. For my use cases this is not an acceptable option. 77 | 78 | Now the console in the debugger works as expected, with completions, evaluations and console 79 | logging from scripts, stepping in/out/over, break points, caller location one click away and 80 | initialization debugging to allow pausing of the script before the JSBridge to JavaScript is 81 | established. Having a real debugger makes minced meat of script initialization issues. 82 | 83 | The current version is the the code I use in my [IntelliJ IDEA] plugin, [Markdown Navigator]. 84 | With any functionality specific to my project added using the API of this library. 85 | 86 | If you are working with JavaFX WebView scripts you need this functionality ASAP. Bugs that took 87 | hours to figure out now take literally seconds to minutes without any recompilation or major log 88 | reading. Take a look at the [Web View Debug Sample] application to see what you get and how to 89 | add it to your code. 90 | 91 | #### What is working 92 | 93 | * all `console` functions: `assert`, `clear`, `count`, `debug`, `dir`, `dirxml`, `error`, 94 | `exception`, `group`, `groupCollapsed`, `groupEnd`, `info`, `log`, `profile`, `profileEnd`, 95 | `select`, `table`, `time`, `timeEnd`, `trace`, `warn`. With added extras to output to the Java 96 | console: `print`, `println` 97 | * all commandLineAPI functions implemented by JavaFX WebView: `$`, `$$`, `$x`, `dir`, `dirxml`, 98 | `keys`, `values`, `profile`, `profileEnd`, `table`, `monitorEvents`, `unmonitorEvents`, 99 | `inspect`, `copy`, `clear`, `getEventListeners`, `$0`, `$_`, `$exception` 100 | * Some limitations do exist because `Runtime.evaluate` is simulated. Otherwise, there was no 101 | way to get the stack frame for any console log calls that would result from the evaluation. 102 | Mainly, it caused the application to exit with a core dump more often than not. I figured it 103 | was more important to have the caller location for console logs than for the commandLineAPI 104 | calls. 105 | * No longer necessary to instrument the page to have `JSBridge` support code working. Only 106 | requires a page reload from WebView or Dev Tools. 107 | * Ability to debug break on page reload to debug scripts running before JSBridge is established 108 | * Safely stop debug session from Dev Tools or WebView side. This should have been easy but 109 | turned out to be the hardest part to solve. Any wrong moves or dangling references would bring 110 | down the whole application with a core-dump message. 111 | 112 | #### In progress 113 | 114 | * highlighting of elements hovered over in the element tree of dev tools. Node resolution is 115 | working. Kludged highlighting by setting a class on the element with translucent background 116 | color defined instead of using an overlay element or the proper margin, borders, padding and 117 | container size. 118 | 119 | #### Not done 120 | 121 | * Debugger paused overlay 122 | 123 | #### Probably not doable 124 | 125 | * profiling not available. Don't know enough about what's needed to say whether it is doable. 126 | 127 | ### JSBridge Provided Debugging Support 128 | 129 | The missing functionality from the WebView debugger is implemented via a proxy that gets between 130 | chrome dev tools and the debugger to fill in the blanks and to massage the conversation allowing 131 | chrome dev tools to do their magic. 132 | 133 | For this to work, some JavaScript and the `JSBridge` instance need to work together to provide 134 | the essential glue. The implementation is confined to the initialization script. Most other 135 | scripts can be oblivious to whether the `JSBridge` is established or not. Console log api is one 136 | of the missing pieces in the WebView debugger, any console log calls made before the `JSBridge` 137 | to Java is established **will not have caller identification** and will instead point to the 138 | initialization code that played back the cached log calls generated before the connection was 139 | established. 140 | 141 | The `JSBridge` implementation also provides a mechanism for data persistence between page 142 | reloads. It is generic enough if all the data you need to persist can be `JSON.stringify'd` 143 | because the implementation does a call back to the WebView engine to serialize the passed in 144 | state argument. This text will need to be inserted into the generated HTML page to allow scripts 145 | access to their state before the `JSBridge` is hooked in. Scripts can also register for a 146 | callback when the `JSBridge` is established. 147 | 148 | The down side of the latter approach, is by the time this happens, WebView has already visually 149 | updated the page. If the script is responsible for any visual modification of the page based on 150 | persisted state then the unmodified version will flash on screen before the script is run. 151 | 152 | Allowing scripts to get their state before `JSBridge` is established makes for smoother page 153 | refresh. 154 | 155 | ### Getting Full Featured Debugging 156 | 157 | This requires a little support from the Java to JavaScript bridge and the debug proxy. See the 158 | [Web View Debug Sample] application for an example. 159 | 160 | I have not tried it with Java 9, and suspect that the debug protocol has changed and the proxy 161 | may not work with it without modifications. 162 | 163 | However, these are the instructions to compile for Java 9 WebView debugger access as given by 164 | [mohamnag/javafx_webview_debugger]. 165 | 166 | `WebEngine.impl_getDebugger()` is an internal API and is subject to change which is happened in 167 | Java 9. So if you are using Java 9, you need to use following code instead to start the debug 168 | server. 169 | 170 | :information_source: This code is now part of `DevToolsDebugProxy` and if it works on the JRE then debugging will 171 | be available. Otherwise, it will not. 172 | 173 | ```java 174 | Class webEngineClazz = WebEngine.class; 175 | 176 | Field debuggerField = webEngineClazz.getDeclaredField("debugger"); 177 | debuggerField.setAccessible(true); 178 | 179 | Debugger debugger = (Debugger) debuggerField.get(webView.getEngine()); 180 | DevToolsDebuggerServer devToolsDebuggerServer.startDebugServer(debugger, WEBVIEW_DEBUG_PORT, 0, null, null); 181 | ``` 182 | 183 | For this to work, you have to pass this parameter to Java compiler: `--add-exports 184 | javafx.web/com.sun.javafx.scene.web=ALL-UNNAMED`. 185 | 186 | As examples, this can be done for Maven as follows: 187 | 188 | ```xml 189 | 190 | org.apache.maven.plugins 191 | maven-compiler-plugin 192 | 3.7.0 193 | 194 | 9 195 | 9 196 | 197 | --add-exports 198 | javafx.web/com.sun.javafx.scene.web=ALL-UNNAMED 199 | 200 | 201 | 202 | ``` 203 | 204 | or for IntelliJ under **Additional command line parameters** in **Preferences > Build, 205 | Execution, Deployment > Compiler > Java Compiler**. 206 | 207 | [IntelliJ IDEA]: http://www.jetbrains.com/idea 208 | [Markdown Navigator]: http://vladsch.com/product/markdown-navigator 209 | [mohamnag/javafx_webview_debugger]: https://github.com/mohamnag/javafx_webview_debugger 210 | [Web View Debug Sample]: https://github.com/vsch/WebViewDebugSample 211 | [Javafx Web View Debugger Readme]: https://github.com/vsch/Javafx-WebView-Debugger/blob/master/README.md 212 | [JavaFx WebView Debugger]: https://github.com/vsch/Javafx-WebView-Debugger 213 | [Kotlin]: https://kotlinlang.org 214 | [TooTallNate/Java-WebSocket]: https://github.com/TooTallNate/Java-WebSocket 215 | [WebViewDebugSample.jar]: https://github.com/vsch/WebViewDebugSample/raw/master/WebViewDebugSample.jar 216 | 217 | -------------------------------------------------------------------------------- /VERSION.md: -------------------------------------------------------------------------------- 1 | ## JavaFx WebView Debugger 2 | 3 | [TOC levels=3,6]: # "Version History" 4 | 5 | ### Version History 6 | - [JavaFx WebView Debugger](#javafx-webview-debugger) 7 | - [0.8.6](#086) 8 | - [0.8.0](#080) 9 | - [0.7.8](#078) 10 | - [0.7.6](#076) 11 | - [0.7.4](#074) 12 | - [0.7.2](#072) 13 | - [0.7.0](#070) 14 | - [0.6.10](#0610) 15 | - [0.6.8](#068) 16 | - [0.6.6](#066) 17 | - [0.6.4](#064) 18 | - [0.6.2](#062) 19 | - [0.6.0](#060) 20 | - [0.5.12](#0512) 21 | - [0.5.10](#0510) 22 | - [0.5.8](#058) 23 | - [0.5.6](#056) 24 | 25 | 26 | ### 0.8.6 27 | 28 | * Fix: add `Throwable` to catch clause when trying to get debugger interface to handle alternate 29 | JavaFX implementations. 30 | 31 | ### 0.8.0 32 | 33 | * Fix: remove dependency on Log4j and add interface to logger handler by app 34 | 35 | ### 0.7.8 36 | 37 | * Fix: update to boxed json 0.5.30 38 | 39 | ### 0.7.6 40 | 41 | * Fix: update to boxed json 0.5.28 42 | 43 | ### 0.7.4 44 | 45 | * Fix: update to boxed json 0.5.24 46 | 47 | ### 0.7.2 48 | 49 | * Fix: update to boxed json 0.5.22 50 | 51 | ### 0.7.0 52 | 53 | * Fix: change constructor params from `Debugger` to `WebEngine` so that `DevToolsDebugProxy` is 54 | responsible for obtaining the debugger instance, if it can. 55 | * Fix: change debug proxy debugger field to nullable to allow the rest of the app to work even 56 | if obtaining the debugger is not possible on the JRE version. 57 | 58 | ### 0.6.10 59 | 60 | * Fix: bump up boxed-json version to 0.5.20 61 | 62 | ### 0.6.8 63 | 64 | * Fix: bump up boxed-json version to 0.5.18 65 | 66 | ### 0.6.6 67 | 68 | * Add: `suppressNoMarkdownException` arg to `DevToolsDebuggerJsBridge` constructor to use JS for 69 | connecting JsBridge which will fail silently if `markdownNavigator` variable is not defined or 70 | is false. 71 | 72 | ### 0.6.4 73 | 74 | * Fix: change log error to warn in JS if using `markdownNavigator.setJsBridge()` with script load 75 | failure. 76 | 77 | ### 0.6.2 78 | 79 | * Fix: change lambdas to functions to have `arguments` available (causing exception in JetBrains 80 | Open JDK 1.8.0_152-release-1293-b10 x86_64 81 | 82 | ### 0.6.0 83 | 84 | * Fix: add debugger argument to `DevToolsDebuggerJsBridge` constructor to allow for Java 9 or 85 | other java version specific debugger use. 86 | 87 | ### 0.5.12 88 | 89 | * Fix: dev tools console commands were not properly converted from JSON to strings, and did not 90 | unescape the `\"` causing exception during eval. 91 | 92 | ### 0.5.10 93 | 94 | * boxed-json fix incorporated. 95 | 96 | ### 0.5.8 97 | 98 | * Add: `JfxDebuggerAccess.onConnectionOpen()` to call back on open connection to debugger 99 | * Add: `JfxDebuggerAccess.onConnectionClosed()` to call back on close connection to debugger 100 | * Fix: page reload in `DevToolsDebuggerJsBridge` Not on FX application thread exception. 101 | 102 | ### 0.5.6 103 | 104 | First working maven version 105 | 106 | -------------------------------------------------------------------------------- /assets/RoughLoadSequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsch/Javafx-WebView-Debugger/8c13d118935b3a6d6f363988a61d4cbc402dfb42/assets/RoughLoadSequence.png -------------------------------------------------------------------------------- /assets/RoughLoadSequence.scap: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Left 7 | 8 | 9 | JavaFX WebView Content Load 10 | 1, 11 11 | 1 12 | 13 | 14 | 15 | Left 16 | 17 | 18 | JavaFX WebView: Worker State == SUCEEDED 19 | 0, 3 20 | 3 21 | 22 | 23 | 24 | Left 25 | 26 | 27 | Java: 28 | DevToolsDebuggerJsBrindge.connectJSBridge() 29 | 1, 6 30 | 6 31 | 32 | 33 | 34 | Left 35 | 36 | 37 | JavaScript calls which require JsBridge are accumulated to run when JsBridge is setup. 38 | 39 | 40 | 41 | Left 42 | 43 | 44 | JavaScript: 45 | markdownNavigator.setJsBridge() 46 | 47 | completes initialization and runs accumulated scripts which require JsBridge. 48 | 3 49 | 50 | 51 | 52 | Left 53 | 54 | 55 | JavaScript: 56 | markdown-navigator.js 57 | 0 58 | 59 | 60 | 61 | 62 | 66 | 69 | 74 | 79 | 84 | 89 | 94 | 95 | 96 | 1.0 1.0 1.0 97 | Helvetica 98 | 0.0 0.0 0.0 99 | 100 | 101 | -------------------------------------------------------------------------------- /images/DevTools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsch/Javafx-WebView-Debugger/8c13d118935b3a6d6f363988a61d4cbc402dfb42/images/DevTools.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 12 | 13 | com.vladsch.javafx-webview-debugger 14 | javafx-webview-debugger 15 | 0.8.6 16 | Javafx WebView Debugger 17 | 18 | Full function debugging implementation for JavaFX WebView using Chrome Dev Tools via web-socket server 19 | 20 | https://github.com/vsch/Javafx-WebView-Debugger 21 | 22 | 23 | 24 | com.vladsch.boxed-json 25 | boxed-json 26 | 0.5.32 27 | 28 | 29 | 30 | org.java-websocket 31 | Java-WebSocket 32 | 1.3.7 33 | 34 | 35 | 36 | 37 | org.jetbrains 38 | annotations 39 | 15.0 40 | 41 | 42 | 43 | junit 44 | junit 45 | 4.12 46 | test 47 | 48 | 49 | 50 | 51 | 52 | MIT license 53 | http://opensource.org/licenses/MIT 54 | repo 55 | 56 | 57 | 58 | 59 | 60 | Vladimir Schneider 61 | vladimir@vladsch.com 62 | vladsch.com 63 | https://vladsch.com/ 64 | 65 | 66 | 67 | 68 | scm:git:git@github.com:vsch/Javafx-WebView-Debugger.git 69 | scm:git:git@github.com:vsch/Javafx-WebView-Debugger.git 70 | git@github.com:vsch/Javafx-WebView-Debugger 71 | HEAD 72 | 73 | 74 | 75 | UTF-8 76 | ${project.basedir}/target/apidocs/ 77 | 78 | false 79 | 80 | 81 | 82 | 83 | ossrh 84 | https://oss.sonatype.org/content/repositories/snapshots 85 | 86 | 87 | ossrh 88 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 89 | 90 | 91 | 92 | 93 | 94 | 95 | org.codehaus.mojo 96 | versions-maven-plugin 97 | 2.3 98 | 99 | false 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-gpg-plugin 105 | 1.6 106 | 107 | 108 | sign-artifacts 109 | verify 110 | 111 | sign 112 | 113 | 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-compiler-plugin 119 | 3.7.0 120 | 121 | 8 122 | 8 123 | 124 | 125 | 126 | maven-deploy-plugin 127 | 2.8.2 128 | 129 | true 130 | 131 | 132 | 133 | maven-source-plugin 134 | 2.2.1 135 | 136 | 137 | package-sources 138 | package 139 | 140 | jar 141 | 142 | 143 | 144 | 145 | 146 | maven-javadoc-plugin 147 | 2.10.4 148 | 149 | 150 | package-javadoc 151 | package 152 | 153 | jar 154 | 155 | 156 | 157 | 158 | 159 | org.sonatype.plugins 160 | nexus-staging-maven-plugin 161 | 1.6 162 | true 163 | 164 | ossrh 165 | https://oss.sonatype.org/ 166 | 167 | 168 | 169 | deploy-to-sonatype 170 | deploy 171 | 172 | deploy 173 | 176 | 177 | 178 | 179 | 180 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/DevToolsDebugProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import com.sun.javafx.application.PlatformImpl; 29 | import com.sun.javafx.scene.web.Debugger; 30 | import com.vladsch.boxed.json.BoxedJsArray; 31 | import com.vladsch.boxed.json.BoxedJsNumber; 32 | import com.vladsch.boxed.json.BoxedJsObject; 33 | import com.vladsch.boxed.json.BoxedJsString; 34 | import com.vladsch.boxed.json.BoxedJsValue; 35 | import com.vladsch.boxed.json.BoxedJson; 36 | import com.vladsch.boxed.json.MutableJsArray; 37 | import com.vladsch.boxed.json.MutableJsObject; 38 | import javafx.application.Platform; 39 | import javafx.scene.web.WebEngine; 40 | import javafx.util.Callback; 41 | import netscape.javascript.JSException; 42 | import netscape.javascript.JSObject; 43 | import org.jetbrains.annotations.NotNull; 44 | import org.jetbrains.annotations.Nullable; 45 | 46 | import javax.json.JsonValue; 47 | import javax.swing.SwingUtilities; 48 | import java.lang.reflect.Field; 49 | import java.util.ArrayList; 50 | import java.util.Arrays; 51 | import java.util.HashMap; 52 | import java.util.HashSet; 53 | import java.util.concurrent.atomic.AtomicBoolean; 54 | import java.util.function.Consumer; 55 | 56 | public class DevToolsDebugProxy implements Debugger, JfxDebuggerProxy, Callback { 57 | final static int RUNTIME_COMPILE_SCRIPT = 1; 58 | final static int RUNTIME_LOG_API = 2; 59 | final static int RUNTIME_LOG_STACK = 3; // resume after stack trace 60 | final static int RUNTIME_SKIP = 4; // just absorb it 61 | final static int RUNTIME_EVALUATE_SCRIPT = 5; // just absorb it 62 | final static int DEBUGGER_PAUSED = 6; // just absorb it 63 | final static int BREAK_POINT_REMOVE = 7; 64 | final static int REQUEST_JS_BRIDGE = 8; 65 | final static int DOM_GET_DOCUMENT = 9; // result is dom get document response 66 | 67 | private static final String EMPTY_EVAL_SCRIPT = "// injected eval to pause immediately"; 68 | private static final String EMPTY_EVAL_STEP_SCRIPT = "\"\";"; 69 | 70 | final @Nullable Debugger myDebugger; 71 | final @NotNull JfxDebuggerAccess myJfxDebuggerAccess; 72 | Callback myCallback; 73 | final HashMap myAsyncIdMap = new HashMap<>(); // mapping of result messages waiting reception from the debugger 74 | final HashMap myAsyncResultMap = new HashMap<>(); // which results we need to massage 75 | int myDebuggerId; // id of next debugger message 76 | int myLastPageContextId; 77 | int mySendNesting = 0; 78 | String mySendNestingIndent = ""; 79 | private boolean myWaitingForEvaluateScript = false; 80 | private final ArrayList myQueuedLogRequests = new ArrayList<>(); 81 | private Runnable myOnEvalDoneRunnable = null; 82 | private Runnable myOnDebuggerResumedRunnable = null; 83 | private boolean myProcessingLogRequest = false; 84 | private Consumer myOnPausedParamsRunnable = null; 85 | private Consumer myOnPageContextCreatedRunnable = null; 86 | private DebugOnLoad myDebugOnLoad = DebugOnLoad.NONE; 87 | private final HashSet myOldPageContextIds = new HashSet<>(); 88 | private boolean mySuppressPageReloadRequest; // when page reload requested from javafx updater not debugger and we don't want to receive page reloading message for it 89 | private DebuggerState myDebuggerState; 90 | private BoxedJsObject myRuntimeEvaluateArgResult; 91 | final private AtomicBoolean myDebuggerIsPaused = new AtomicBoolean(false); 92 | private boolean myPageReloadStarted = false; // true if started loading but did not yet complete, to prevent nested reload requests 93 | final private HashMap myBreakpoints = new HashMap<>(); 94 | final private HashMap myBreakpointsToRemove = new HashMap<>(); // request to break point id 95 | private boolean myIsEnabled; 96 | private boolean myIsShuttingDown; 97 | private HashMap myNodeIdMap = new HashMap<>(); // node id to params of DOM.setChildNodes method from webView with "parentId" added to each node and "ordinal" position in parent's children 98 | private int myRootNodeId = 0; // this is the document node id 99 | final LogHandler LOG = LogHandler.getInstance(); 100 | 101 | // reflects the last command received for debugger 102 | // needed so that after pausing for console log stack trace, we can use the same 103 | // command instead of resume 104 | public enum DebuggerState { 105 | PAUSED("Debugger.pause"), 106 | RUNNING("Debugger.resume"), 107 | STEP_OVER("Debugger.stepOver"), 108 | STEP_INTO("Debugger.stepInto"), 109 | STEP_OUT("Debugger.stepOut"); 110 | 111 | final public String method; 112 | 113 | DebuggerState(final String method) { 114 | this.method = method; 115 | } 116 | } 117 | 118 | @Nullable 119 | public static Debugger getDebugger(@NotNull WebEngine engine) { 120 | try { 121 | // DEPRECATED: no replacement 122 | @SuppressWarnings("deprecation") 123 | Debugger debugger = engine.impl_getDebugger(); 124 | return debugger; 125 | } catch (NoSuchMethodError error) { 126 | Class webEngineClazz = WebEngine.class; 127 | 128 | Field debuggerField = null; 129 | try { 130 | debuggerField = webEngineClazz.getDeclaredField("debugger"); 131 | debuggerField.setAccessible(true); 132 | return (Debugger) debuggerField.get(engine); 133 | } catch (Throwable e) { 134 | e.printStackTrace(); 135 | return null; 136 | } 137 | } 138 | } 139 | 140 | public DevToolsDebugProxy(@NotNull final WebEngine engine, @NotNull final JfxDebuggerAccess jfxDebuggerAccess) { 141 | myJfxDebuggerAccess = jfxDebuggerAccess; 142 | myDebugger = getDebugger(engine); 143 | clearState(); 144 | 145 | if (myDebugger != null) { 146 | myDebugger.setMessageCallback(this); 147 | } 148 | } 149 | 150 | private void clearState() { 151 | myDebuggerId = 1; 152 | myLastPageContextId = 0; 153 | myAsyncIdMap.clear(); 154 | myAsyncResultMap.clear(); 155 | mySendNesting = 0; 156 | mySendNestingIndent = ""; 157 | mySuppressPageReloadRequest = false; 158 | myDebuggerState = DebuggerState.RUNNING; 159 | clearOnPageReload(); 160 | myDebuggerIsPaused.set(false); 161 | myPageReloadStarted = false; 162 | myBreakpoints.clear(); 163 | myBreakpointsToRemove.clear(); 164 | myIsShuttingDown = false; 165 | myOnPageContextCreatedRunnable = null; 166 | } 167 | 168 | private void clearOnPageReload() { 169 | myQueuedLogRequests.clear(); 170 | myWaitingForEvaluateScript = false; 171 | myOnEvalDoneRunnable = null; 172 | myOnPausedParamsRunnable = null; 173 | myProcessingLogRequest = false; 174 | myRuntimeEvaluateArgResult = null; 175 | myAsyncResultMap.clear(); 176 | myOnDebuggerResumedRunnable = null; 177 | myNodeIdMap.clear(); 178 | myRootNodeId = 0; 179 | myJfxDebuggerAccess.clearArg(); 180 | } 181 | 182 | @Override 183 | public boolean isDebuggerPaused() { 184 | return myDebuggerIsPaused.get(); 185 | } 186 | 187 | private void logMessage(final String message) { 188 | if (LOG.isDebugEnabled()) System.out.println(message); 189 | } 190 | 191 | @Override 192 | public void releaseDebugger(final boolean shuttingDown, @Nullable Runnable runnable) { 193 | if (!myIsEnabled) return; 194 | 195 | myIsShuttingDown = shuttingDown; 196 | if (myDebuggerIsPaused.get()) { 197 | Runnable action = () -> { 198 | if (myDebugger != null && myDebugger.isEnabled()) { 199 | if (myOnDebuggerResumedRunnable == null) { 200 | myOnDebuggerResumedRunnable = runnable; 201 | } else if (runnable != null) { 202 | // already being paused, chain them 203 | Runnable other = myOnDebuggerResumedRunnable; 204 | myOnDebuggerResumedRunnable = () -> { 205 | other.run(); 206 | runnable.run(); 207 | }; 208 | } 209 | debuggerSend(String.format("{\"id\":%d,\"method\":\"%s\"}", myDebuggerId++, DebuggerState.RUNNING.method), EMPTY_EVAL_SCRIPT); 210 | } else if (runnable != null) { 211 | runnable.run(); 212 | } 213 | }; 214 | 215 | if (Platform.isFxApplicationThread()) { 216 | action.run(); 217 | } else { 218 | PlatformImpl.runAndWait(action); 219 | } 220 | } else if (runnable != null) { 221 | runnable.run(); 222 | } 223 | } 224 | 225 | @Override 226 | public void onOpen() { 227 | myIsShuttingDown = false; 228 | Platform.runLater(myJfxDebuggerAccess::onConnectionOpen); 229 | } 230 | 231 | @Override 232 | public void setDebugOnLoad(final DebugOnLoad debugOnLoad) { 233 | myDebugOnLoad = debugOnLoad; 234 | } 235 | 236 | @Override 237 | public DebugOnLoad getDebugOnLoad() { 238 | return myDebugOnLoad; 239 | } 240 | 241 | @Override 242 | public void onClosed(final int code, final String reason, final boolean remote) { 243 | if (!myIsEnabled) return; 244 | 245 | //myIsShuttingDown = true; // this messes it up, don't do it, shutdown is when the user turns off debugging 246 | 247 | // FIX: return to not yet connected condition 248 | removeAllBreakpoints(() -> { 249 | releaseDebugger(true, () -> { 250 | //noinspection Convert2MethodRef 251 | clearOnPageReload(); 252 | }); 253 | }); 254 | 255 | Platform.runLater(myJfxDebuggerAccess::onConnectionClosed); 256 | } 257 | 258 | @Override 259 | public void removeAllBreakpoints(final @Nullable Runnable runAfter) { 260 | if (!myIsEnabled || myDebugger == null) return; 261 | 262 | // must be called on javafx thread, debugger can be paused or not 263 | Platform.runLater(() -> { 264 | // one of those left over break points 265 | // Before resuming, remove all break points 266 | ArrayList breakPoints = new ArrayList<>(myBreakpoints.keySet()); 267 | BoxedJsObject jsRemoveBreakPoint = BoxedJson.boxedFrom("{\"id\":454,\"method\":\"Debugger.removeBreakpoint\",\"params\":{\"breakpointId\":\"file:///Users/vlad/src/sites/public/mn-resources/admonition.js:16:0\"}}"); 268 | for (String breakpointId : breakPoints) { 269 | // {"id":454,"method":"Debugger.removeBreakpoint","params":{"breakpointId":"file:///Users/vlad/src/sites/public/mn-resources/admonition.js:16:0"}} 270 | jsRemoveBreakPoint.evalSet("id", myDebuggerId++) 271 | .evalSet("params.breakpointId", breakpointId) 272 | ; 273 | String removeParam = jsRemoveBreakPoint.toString(); 274 | logMessage(String.format("Removing all breakpoints %s", removeParam)); 275 | myDebugger.sendMessage(removeParam); 276 | } 277 | myBreakpoints.clear(); 278 | myBreakpointsToRemove.clear(); 279 | 280 | if (runAfter != null) { 281 | runAfter.run(); 282 | } 283 | }); 284 | } 285 | 286 | @Override 287 | public void pageReloading() { 288 | if (!myIsEnabled) return; 289 | 290 | mySuppressPageReloadRequest = true; 291 | myPageReloadStarted = true; 292 | } 293 | 294 | @Override 295 | public void reloadPage() { 296 | if (!myIsEnabled) return; 297 | 298 | // send page reload so it can be debugged, does not work 299 | if (!myPageReloadStarted) { 300 | myPageReloadStarted = true; 301 | Platform.runLater(() -> { 302 | mySuppressPageReloadRequest = false; 303 | logMessage(String.format("Sending page reload, request %d", myDebuggerId)); 304 | debuggerSend(String.format("{\"id\":%d,\"method\":\"Page.reload\", \"params\": {\"ignoreCache\":false}}", myDebuggerId++), null); 305 | }); 306 | } 307 | } 308 | 309 | @Override 310 | public void pageLoadComplete() { 311 | myPageReloadStarted = false; 312 | } 313 | 314 | void debuggerSend(String message, final String evalAfter) { 315 | if (myDebugger != null) { 316 | mySendNesting++; 317 | String wasIndent = mySendNestingIndent; 318 | mySendNestingIndent += " "; 319 | logMessage(String.format("%sSending %s", mySendNestingIndent, message)); 320 | myDebugger.sendMessage(message); 321 | if (evalAfter != null) { 322 | yieldDebugger(() -> { 323 | myJfxDebuggerAccess.eval(evalAfter); 324 | }); 325 | } 326 | mySendNestingIndent = wasIndent; 327 | mySendNesting--; 328 | } 329 | } 330 | 331 | /** 332 | * Called before the passed in parameters are funnelled as a dummy script for evaluation 333 | * In the callers context, the messages coming in from WebView debugger should be monitored and 334 | * the runtime console api message generated from its parameters 335 | * 336 | * @param type console log call type 337 | * @param timestamp timestamp in nanoseconds since epoch 1970/1/1. 338 | * @param args Arguments passed to the log function 339 | */ 340 | @Override 341 | public void log(final String type, final long timestamp, final JSObject args) { 342 | if (!myIsEnabled || myIsShuttingDown) return; 343 | 344 | // {"method":"Runtime.consoleAPICalled","params":{"type":"warning","args":[{"type":"string","value":"warning"}],"executionContextId":30,"timestamp":1519047166210.763,"stackTrace":{"callFrames":[{"functionName":"","scriptId":"684","url":"","lineNumber":0,"columnNumber":8}]}}} 345 | // return string to append to end of the param call so we know what Runtime.evaluate text to look for to accumulate arguments 346 | // the string is a comment // #frameId.N where N 0...args.length, with the last one being just #frameId, marking that we can generate and send the consoleAPI called message 347 | // 348 | int iMax = (int) args.getMember("length"); 349 | Object[] argList = new Object[iMax]; 350 | for (int i = 0; i < iMax; i++) { 351 | argList[i] = args.getSlot(i); 352 | } 353 | 354 | final JfxConsoleApiArgs consoleArgs = new JfxConsoleApiArgs(argList, type, timestamp); 355 | 356 | if (myWaitingForEvaluateScript) { 357 | // could not be sure to handle the evaluate args properly, so let the Runtime.evaluate through to the debugger 358 | // and it contained a log somewhere in the call tree. Won't get correct call location but everything else 359 | // is fine. 360 | myProcessingLogRequest = true; 361 | if (myQueuedLogRequests.isEmpty()) { 362 | // first log request in the Runtime.evaluate, it to the queue so we know the next one is not first 363 | myQueuedLogRequests.add(consoleArgs); 364 | myOnEvalDoneRunnable = () -> { 365 | pause(pausedParam -> { 366 | consoleArgs.setPausedParam(pausedParam); 367 | // drop the first it is already in consoleArgs 368 | JfxConsoleApiArgs tmp = myQueuedLogRequests.remove(0); 369 | assert tmp == consoleArgs; 370 | collectConsoleAPIParams(consoleArgs); 371 | }); 372 | }; 373 | } else { 374 | // the rest will be run after the first 375 | myQueuedLogRequests.add(consoleArgs); 376 | } 377 | } else { 378 | if (myProcessingLogRequest) { 379 | // queue it up, they get log debug params if there are any 380 | myQueuedLogRequests.add(consoleArgs); 381 | } else { 382 | myProcessingLogRequest = true; 383 | pause(pausedParam -> { 384 | consoleArgs.setPausedParam(pausedParam); 385 | collectConsoleAPIParams(consoleArgs); 386 | }); 387 | } 388 | } 389 | } 390 | 391 | private void yieldDebugger(final Runnable runnable) { 392 | //ApplicationManager.getApplication().invokeLater(() -> { 393 | SwingUtilities.invokeLater(() -> { 394 | Platform.runLater(() -> { 395 | //noinspection Convert2MethodRef,FunctionalExpressionCanBeFolded 396 | runnable.run(); 397 | }); 398 | }); 399 | } 400 | 401 | private void pause(@NotNull Consumer onPausedRunnable) { 402 | if (myWaitingForEvaluateScript || myOnPausedParamsRunnable != null) { 403 | throw new IllegalStateException("Pause request nested or during waiting on eval"); 404 | } 405 | 406 | final int debuggerId = myDebuggerId++; 407 | myAsyncResultMap.put(debuggerId, DEBUGGER_PAUSED); // skip the result of this pause request 408 | logMessage(String.format("Pausing debugger, request %d", debuggerId)); 409 | 410 | myOnPausedParamsRunnable = (pausedParams) -> { 411 | logMessage(String.format("Running onPaused callback for request %d", debuggerId)); 412 | onPausedRunnable.accept(pausedParams); 413 | }; 414 | 415 | debuggerSend(String.format("{\"id\":%d,\"method\":\"Debugger.pause\"}", debuggerId), EMPTY_EVAL_SCRIPT); 416 | } 417 | 418 | @Override 419 | public void debugBreak() { 420 | if (!myIsEnabled || myIsShuttingDown) return; 421 | 422 | debugBreak(null); 423 | } 424 | 425 | public void debugBreak(String evalAfter) { 426 | logMessage(String.format("DebugBreak debugger, request %d", myDebuggerId)); 427 | myDebuggerState = DebuggerState.PAUSED; 428 | debuggerSend(String.format("{\"id\":%d,\"method\":\"Debugger.pause\"}", myDebuggerId++), evalAfter); 429 | } 430 | 431 | private BoxedJsObject getArgParam(Object arg) { 432 | return getArgParam(arg, argParamJson()); 433 | } 434 | 435 | private BoxedJsObject getArgParam(Object arg, BoxedJsObject json) { 436 | //{"result":{"result":{"type":"object","objectId":"{\"injectedScriptId\":1,\"id\":5}","className":"Object","description":"Object","preview":{"type":"object","description":"Object","lossless":true,"properties":[{"name":"x","type":"number","value":"0"},{"name":"y","type":"number","value":"79.27999999999997"}]}},"wasThrown":false},"id":36} 437 | //{"id":36,"method":"Runtime.evaluate","params":{"expression":"onLoadScroll","objectGroup":"console","includeCommandLineAPI":true,"silent":false,"contextId":1,"returnByValue":false,"generatePreview":true,"userGesture":true,"awaitPromise":false}} 438 | 439 | final String argScript = myJfxDebuggerAccess.setArg(arg); 440 | json.evalSet("id", myDebuggerId) 441 | .evalSet("params.expression", argScript) 442 | .evalSet("params.contextId", myLastPageContextId); 443 | final String message = json.toString(); 444 | 445 | myAsyncResultMap.put(myDebuggerId, RUNTIME_LOG_API); 446 | myDebuggerId++; 447 | 448 | myRuntimeEvaluateArgResult = null; 449 | 450 | debuggerSend(message, null); 451 | 452 | assert myRuntimeEvaluateArgResult != null; 453 | BoxedJsObject result = myRuntimeEvaluateArgResult; 454 | myRuntimeEvaluateArgResult = null; 455 | return result; 456 | } 457 | 458 | private BoxedJsObject argParamJson() { 459 | final String evalScript = "{\"id\":0,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"\",\"objectGroup\":\"console\",\"includeCommandLineAPI\":true,\"silent\":false,\"contextId\":1,\"returnByValue\":false,\"generatePreview\":true,\"userGesture\":true,\"awaitPromise\":false}}"; 460 | return BoxedJson.boxedFrom(evalScript); 461 | } 462 | 463 | private void collectConsoleAPIParams(JfxConsoleApiArgs consoleArgs) { 464 | final BoxedJsObject json = argParamJson(); 465 | final String firstPauseParams = consoleArgs.getPausedParam(); 466 | assert firstPauseParams != null; 467 | 468 | do { 469 | // debugger was paused, we can now evaluate on call frame 470 | final Object[] args = consoleArgs.getArgs(); 471 | final int iMax = args.length; 472 | 473 | for (int i = 0; i < iMax; i++) { 474 | final Object arg = args[i]; 475 | // create Runtime.evaluate with this message 476 | 477 | logMessage(String.format("Evaluating result param[%d], request %d", i, myDebuggerId)); 478 | BoxedJsObject result = getArgParam(arg, json); 479 | if (result == null) return; 480 | 481 | final BoxedJsObject jsParam = result.eval("result.result").asJsObject(); 482 | consoleArgs.setParamJson(i, jsParam); 483 | } 484 | 485 | if (consoleArgs.getPausedParam() == null) { 486 | consoleArgs.setPausedParam(firstPauseParams); 487 | } 488 | 489 | sendConsoleAPI(consoleArgs); 490 | if (!myWaitingForEvaluateScript && !myQueuedLogRequests.isEmpty()) { 491 | consoleArgs = myQueuedLogRequests.remove(0); 492 | } else { 493 | // let the end of evaluation or next request log request continue the process 494 | consoleArgs = null; 495 | myProcessingLogRequest = false; 496 | } 497 | } while (consoleArgs != null); 498 | 499 | // resume in a state appropriate to what it was when consoleLog was called. 500 | logMessage(String.format("Resuming debugger after consoleLogAPI, request %d", myDebuggerId)); 501 | boolean wasRunning = myDebuggerState == DebuggerState.RUNNING; 502 | String nextState = wasRunning ? DebuggerState.RUNNING.method : DebuggerState.STEP_OVER.method; 503 | myAsyncResultMap.put(myDebuggerId, RUNTIME_SKIP); // skip the result of this pause request 504 | debuggerSend(String.format("{\"id\":%d,\"method\":\"%s\"}", myDebuggerId++, nextState), wasRunning ? EMPTY_EVAL_SCRIPT : EMPTY_EVAL_STEP_SCRIPT); 505 | } 506 | 507 | private void sendConsoleAPI(JfxConsoleApiArgs consoleArgs) { 508 | // the parameters for consoleLog are not the break point stack params because it disappears before dev tools can get this information 509 | // so the console log api sends resolved information 510 | //{ 511 | // "method": "Runtime.consoleAPICalled", 512 | // "params": { 513 | // "type": "log", 514 | // "args": [ 515 | // { 516 | // "type": "string", 517 | // "value": "test" 518 | // } 519 | // ], 520 | // "executionContextId": 5, 521 | // "timestamp": 1519412254600.44, 522 | // "stackTrace": { 523 | // "callFrames": [ 524 | // { 525 | // "functionName": "", 526 | // "scriptId": "183", 527 | // "url": "", 528 | // "lineNumber": 0, 529 | // "columnNumber": 8 530 | // } 531 | // ] 532 | // } 533 | // } 534 | //} 535 | // 536 | // each call frame in the pausedParams is: 537 | // "callFrameId": "{\"ordinal\":0,\"injectedScriptId\":2}", 538 | // "functionName": "consoleLog", 539 | // "location": { 540 | // "scriptId": "43", 541 | // "lineNumber": 33, 542 | // "columnNumber": 8 543 | // }, 544 | // 545 | // need to convert it to what dev tools expects 546 | // 547 | BoxedJsObject json;//{"method":"Runtime.consoleAPICalled","params":{"type":"warning","args":[{"type":"string","value":"warning"}],"executionContextId":30,"timestamp":1519047166210.763,"stackTrace":{"callFrames":[{"functionName":"","scriptId":"684","url":"","lineNumber":0,"columnNumber":8}]}}} 548 | 549 | json = BoxedJson.boxedFrom(consoleArgs.getPausedParam()); 550 | final BoxedJsArray jsCallFrames = json.eval("params.callFrames").asJsArray(); 551 | final MutableJsArray jsStackFrames = new MutableJsArray(jsCallFrames.size()); 552 | int skipFrames = jsCallFrames.size() > 1 ? 1 : 0; 553 | for (JsonValue jsonValue : jsCallFrames) { 554 | BoxedJsObject jsCallFrame = BoxedJson.boxedOf(jsonValue).asJsObject(); 555 | if (jsCallFrame.isValid() && --skipFrames < 0) { 556 | BoxedJsObject jsStackFrame = BoxedJson.boxedOf(new MutableJsObject(5)); 557 | jsStackFrame.evalSet("functionName", jsCallFrame.get("functionName").asJsString()) 558 | .evalSet("scriptId", jsCallFrame.eval("location.scriptId").asJsString()) 559 | .evalSet("lineNumber", jsCallFrame.eval("location.lineNumber").asJsNumber()) 560 | .evalSet("columnNumber", jsCallFrame.eval("location.columnNumber").asJsNumber()) 561 | .evalSet("url", "") 562 | ; 563 | 564 | jsStackFrames.add(jsStackFrame); 565 | } 566 | } 567 | 568 | // all args are done, we have our stack frame 569 | MutableJsArray args = new MutableJsArray(consoleArgs.getJsonParams().length); 570 | args.addAll(Arrays.asList(consoleArgs.getJsonParams())); 571 | 572 | final BoxedJsObject consoleApiJson = BoxedJson.boxedFrom("{\"method\":\"Runtime.consoleAPICalled\",\"params\":{\"type\":\"type\",\"args\":[],\"executionContextId\":30,\"stackTrace\":{\"callFrames\":[{\"functionName\":\"\",\"scriptId\":\"684\",\"url\":\"\",\"lineNumber\":0,\"columnNumber\":1}]}}}"); 573 | consoleApiJson.evalSet("params.type", consoleArgs.getLogType()) 574 | .evalSet("params.executionContextId", myLastPageContextId) 575 | .evalSet("params.timestamp", consoleArgs.getTimestamp() / 1000000.0) 576 | .evalSet("params.stackTrace.callFrames", jsStackFrames) 577 | .evalSet("params.args", args) 578 | ; 579 | String dataParam = consoleApiJson.toString(); 580 | 581 | logMessage("Sending console log data " + dataParam); 582 | if (myCallback != null) { 583 | myCallback.call(dataParam); 584 | } 585 | 586 | consoleArgs.clearAll(); 587 | myJfxDebuggerAccess.clearArg(); 588 | } 589 | 590 | /** 591 | * Call back to send message to remote from the debugger 592 | * 593 | * @param param json 594 | * 595 | * @return void 596 | */ 597 | @Override 598 | public Void call(final String param) { 599 | // pre-process results here and possibly change or filter calls to debugger 600 | String changedParam = param; 601 | BoxedJsObject json = BoxedJson.boxedFrom(param); 602 | boolean changed = false; 603 | boolean runOnEvalRunnables = false; 604 | final BoxedJsString method = json.get("method").asJsString(); 605 | BoxedJsNumber jsId = json.getJsonNumber("id"); 606 | 607 | // we need to extract the page id context for evaluation of console.log arguments 608 | // {"method":"Runtime.executionContextCreated","params":{"context":{"id":37,"origin":"https://www.chromium.org","name":"","auxData":{"isDefault":true,"frameId":"(4C61E9CA78FE3BCDFCDBF02580564A1)"}}}} 609 | switch (method.getString()) { 610 | case "Runtime.executionContextCreated": { 611 | // when seeing {"method":"Runtime.executionContextCreated","params":{"context":{"id":6,"isPageContext":true,"name":"","frameId":"0.1"}}} 612 | // then if on request we get the old context id, replace it with the latest page context 613 | final BoxedJsObject jsContext = json.evalJsObject("params.context"); 614 | final BoxedJsNumber contextIdNumber = jsContext.getJsonNumber("id"); 615 | if (jsContext.eval("isPageContext").isTrue() && contextIdNumber.isValid()) { 616 | if (myLastPageContextId > 0) { 617 | myOldPageContextIds.add(myLastPageContextId); 618 | } 619 | myLastPageContextId = contextIdNumber.intValue(); 620 | Consumer onPageContextRunnable = myOnPageContextCreatedRunnable; 621 | myOnPageContextCreatedRunnable = null; 622 | if (onPageContextRunnable != null) { 623 | onPageContextRunnable.accept(myLastPageContextId); 624 | } 625 | } 626 | break; 627 | } 628 | case "Debugger.paused": { 629 | logMessage(String.format("Got debug paused: %s", param)); 630 | myDebuggerIsPaused.set(true); 631 | 632 | final Consumer runnable = myOnPausedParamsRunnable; 633 | myOnPausedParamsRunnable = null; 634 | if (runnable != null) { 635 | yieldDebugger(() -> runnable.accept(param)); 636 | return null; 637 | } 638 | 639 | if (myIsShuttingDown) { 640 | // make it continue so it does not hang the app 641 | yieldDebugger(() -> { 642 | if (myDebugger != null && myIsEnabled) { 643 | // let's see the reason, if it is a break point, we delete it 644 | //{"method":"Debugger.paused","params":{"callFrames":[{"callFrameId":"{\"ordinal\":0,\"injectedScriptId\":3}","functionName":"","location":{"scriptId":"81","lineNumber":10,"columnNumber":16},"scopeChain":[{"object":{"type":"object","objectId":"{\"injectedScriptId\":3,\"id\":174}","className":"JSLexicalEnvironment","description":"JSLexicalEnvironment"},"type":"local"},{"object":{"type":"object","objectId":"{\"injectedScriptId\":3,\"id\":175}","className":"JSLexicalEnvironment","description":"JSLexicalEnvironment"},"type":"closure"},{"object":{"type":"object","objectId":"{\"injectedScriptId\":3,\"id\":176}","className":"JSLexicalEnvironment","description":"JSLexicalEnvironment"},"type":"closure"},{"object":{"type":"object","objectId":"{\"injectedScriptId\":3,\"id\":177}","className":"JSLexicalEnvironment","description":"JSLexicalEnvironment"},"type":"closure"},{"object":{"type":"object","objectId":"{\"injectedScriptId\":3,\"id\":178}","className":"JSLexicalEnvironment","description":"JSLexicalEnvironment"},"type":"closure"},{"object":{"type":"object","objectId":"{\"injectedScriptId\":3,\"id\":179}","className":"Window","description":"Window"},"type":"global"}],"this":{"type":"object","objectId":"{\"injectedScriptId\":3,\"id\":180}","subtype":"node","className":"HTMLDivElement","description":"div.adm-block.adm-example.adm-collapsed"}}],"reason":"Breakpoint","data":{"breakpointId":"file:///Users/vlad/src/sites/public/mn-resources/admonition.js:10:0"}}} 645 | //{ 646 | // "method": "Debugger.paused", 647 | // "params": { 648 | // "reason": "Breakpoint", 649 | // "data": { 650 | // "breakpointId": "file:///Users/vlad/src/sites/public/mn-resources/admonition.js:10:0" 651 | // } 652 | // } 653 | //} 654 | BoxedJsString reason = json.eval("params.reason").asJsString(); 655 | BoxedJsString breakpointId = json.eval("params.data.breakpointId").asJsString(); 656 | if (breakpointId.isValid() && reason.getString().equals("Breakpoint")) { 657 | // remove the thing 658 | // {"id":454,"method":"Debugger.removeBreakpoint","params":{"breakpointId":"file:///Users/vlad/src/sites/public/mn-resources/admonition.js:16:0"}} 659 | BoxedJsObject jsRemoveBreakPoint = BoxedJson.boxedFrom("{\"id\":454,\"method\":\"Debugger.removeBreakpoint\",\"params\":{\"breakpointId\":\"file:///Users/vlad/src/sites/public/mn-resources/admonition.js:16:0\"}}"); 660 | jsRemoveBreakPoint.evalSet("id", myDebuggerId++); 661 | jsRemoveBreakPoint.evalSet("params.breakpointId", breakpointId); 662 | String removeParam = jsRemoveBreakPoint.toString(); 663 | logMessage(String.format("Removing leftover breakpoint %s", removeParam)); 664 | myDebugger.sendMessage(removeParam); 665 | } 666 | // now resume 667 | debuggerSend(String.format("{\"id\":%d,\"method\":\"%s\"}", myDebuggerId++, DebuggerState.RUNNING.method), ""); 668 | } 669 | }); 670 | return null; 671 | } 672 | 673 | break; 674 | } 675 | case "Debugger.resumed": { 676 | myDebuggerIsPaused.set(false); 677 | Runnable runnable = myOnDebuggerResumedRunnable; 678 | myOnDebuggerResumedRunnable = null; 679 | if (runnable != null) { 680 | yieldDebugger(runnable); 681 | } 682 | logMessage(String.format("Got debug resumed: %s", param)); 683 | break; 684 | } 685 | case "Debugger.globalObjectCleared": { 686 | //{"method":"Network.loadingFinished","params":{"requestId":"0.100","timestamp":0.06045897198055172}} 687 | //{"method":"Page.frameStartedLoading","params":{"frameId":"0.1"}} 688 | // {"method":"Debugger.globalObjectCleared"} 689 | myDebuggerState = DebuggerState.RUNNING; 690 | clearOnPageReload(); 691 | 692 | // we use this to inject our custom code into the global space 693 | // to handle things until jsBridge is established. This is just in case 694 | // the page was not instrumented with our code. If it was then it will overwrite 695 | // this injection with a script file 696 | //myDebugOnLoad = false; 697 | // see if we can inject something into the global object before any scripts run 698 | // {"id":250,"method":"Runtime.evaluate","params":{"expression":"window.__MarkdownNavigatorArgs.getConsoleArg()","objectGroup":"console","includeCommandLineAPI":true,"silent":false,"contextId":4,"returnByValue":false,"generatePreview":true,"userGesture":true,"awaitPromise":false}} 699 | myOnPageContextCreatedRunnable = (pageContextId) -> { 700 | if (myDebugOnLoad == DebugOnLoad.ON_INJECT_HELPER) { 701 | // able to debug our injected script if issuing a pause here 702 | // but with external pages does crash 703 | myDebugOnLoad = DebugOnLoad.NONE; 704 | logMessage(String.format("Setting pause after inject helpers, request %d", myDebuggerId)); 705 | myDebuggerState = DebuggerState.PAUSED; 706 | debuggerSend(String.format("{\"id\":%d,\"method\":\"Debugger.pause\"}", myDebuggerId++), null); 707 | } 708 | 709 | BoxedJsObject paramJson = argParamJson(); 710 | //final String argScript = "window.injectedCode = { tests: () => { return \"I'm injected\"; } };"; 711 | final String argScript = myJfxDebuggerAccess.jsBridgeHelperScript(); 712 | paramJson.evalSet("id", myDebuggerId) 713 | .evalSet("params.expression", argScript) 714 | .evalSet("params.includeCommandLineAPI", true) 715 | .evalSet("params.objectGroup", "markdownNavigatorInjected") 716 | .evalSet("params.contextId", pageContextId) 717 | ; 718 | 719 | myAsyncResultMap.put(myDebuggerId, REQUEST_JS_BRIDGE); 720 | 721 | logMessage(String.format("Injecting helper script, request %d", myDebuggerId)); 722 | myDebuggerId++; 723 | debuggerSend(paramJson.toString(), null); 724 | }; 725 | logMessage(String.format("Got Debugger.globalObjectCleared: %s", param)); 726 | break; 727 | } 728 | //case "Page.frameStoppedLoading": { 729 | // // not used 730 | // break; 731 | //} 732 | 733 | case "DOM.setChildNodes": { 734 | BoxedJsNumber jsParentId = json.evalJsNumber("params.parentId"); 735 | BoxedJsArray jsNodes = json.evalJsArray("params.nodes"); 736 | //"parentId":93, 737 | // "nodes": [ 738 | if (jsParentId.isValid() && jsNodes.isValid()) { 739 | logMessage(String.format("Adding children of node: %d", jsParentId.intValue())); 740 | addNodeChildren(jsParentId.intValue(), jsNodes, 0, null, 0); 741 | } else { 742 | // did not add 743 | logMessage(String.format("Did not process children for %s", param)); 744 | break; 745 | } 746 | break; 747 | } 748 | 749 | case "DOM.childNodeRemoved": { 750 | // { 751 | // "name": "childNodeRemoved", 752 | // "parameters": [ 753 | // { 754 | // "name": "parentNodeId", 755 | // "$ref": "NodeId", 756 | // "description": "Parent id." 757 | // }, 758 | // { 759 | // "name": "nodeId", 760 | // "$ref": "NodeId", 761 | // "description": "Id of the node that has been removed." 762 | // } 763 | // ], 764 | // "description": "Mirrors DOMNodeRemoved event." 765 | // } 766 | BoxedJsNumber jsParentId = json.evalJsNumber("params.parentNodeId"); 767 | BoxedJsNumber jsNodeId = json.getJsonNumber("params.nodeId"); 768 | BoxedJsObject jsParentParams = myNodeIdMap.getOrDefault(jsParentId.intValue(), BoxedJsValue.HAD_NULL_OBJECT); 769 | boolean handled = false; 770 | if (jsParentId.isValid() && jsNodeId.isValid() && jsParentParams.isValid()) { 771 | // we now parse for nodes, remove this node and process it as new 772 | logMessage(String.format("Removing child %d of node: %d", jsNodeId.intValue(), jsParentId.intValue())); 773 | BoxedJsArray jsNodes = jsParentParams.getJsonArray("children"); 774 | addNodeChildren(jsParentId.intValue(), jsNodes, jsNodeId.intValue(), null, 0); 775 | handled = true; 776 | } 777 | 778 | if (!handled) { 779 | // did not add 780 | logMessage(String.format("Did not insert child node for %s", param)); 781 | break; 782 | } 783 | break; 784 | } 785 | 786 | case "DOM.childNodeInserted": { 787 | // { 788 | // "name": "childNodeInserted", 789 | // "parameters": [ 790 | // { 791 | // "name": "parentNodeId", 792 | // "$ref": "NodeId", 793 | // "description": "Id of the node that has changed." 794 | // }, 795 | // { 796 | // "name": "previousNodeId", 797 | // "$ref": "NodeId", 798 | // "description": "If of the previous siblint." 799 | // }, 800 | // { 801 | // "name": "node", 802 | // "$ref": "Node", 803 | // "description": "Inserted node data." 804 | // } 805 | // ], 806 | // "description": "Mirrors DOMNodeInserted event." 807 | // }, 808 | BoxedJsObject jsNode = json.evalJsObject("params.node"); 809 | BoxedJsNumber jsPreviousNodeId = json.evalJsNumber("params.previousNodeId"); 810 | BoxedJsNumber jsParentId = json.evalJsNumber("params.parentNodeId"); 811 | BoxedJsObject jsParentParams = myNodeIdMap.getOrDefault(jsParentId.intValue(), BoxedJsValue.HAD_NULL_OBJECT); 812 | BoxedJsArray jsNodes = jsParentParams.getJsonArray("children"); 813 | boolean handled = false; 814 | //"parentId":93, 815 | // "nodes": [ 816 | if (jsNode.isValid() && jsPreviousNodeId.isValid() && jsParentId.isValid() && jsNodes.isValid()) { 817 | logMessage(String.format("Inserting child of node: %d: %s", jsParentId.intValue(), jsNode.toString())); 818 | addNodeChildren(jsParentId.intValue(), jsNodes, 0, jsNode, jsPreviousNodeId.intValue()); 819 | handled = true; 820 | } 821 | 822 | if (!handled) { 823 | // did not add 824 | logMessage(String.format("Did not insert child node for %s", param)); 825 | break; 826 | } 827 | } 828 | } 829 | 830 | BoxedJsObject jsError = json.get("error").asJsObject(); 831 | BoxedJsObject jsResult = json.get("result").asJsObject(); 832 | if (jsId.isValid() && (jsError.isValid() || jsResult.isValid())) { 833 | // may need to further massage the content 834 | int id = jsId.intValue(); 835 | 836 | if (myAsyncResultMap.containsKey(id)) { 837 | // this one is a replacement 838 | int resultType = myAsyncResultMap.remove(id); 839 | switch (resultType) { 840 | case RUNTIME_EVALUATE_SCRIPT: { 841 | runOnEvalRunnables = true; 842 | break; 843 | } 844 | 845 | case RUNTIME_COMPILE_SCRIPT: { 846 | // information from the last Debugger.scriptParsed 847 | // now just harmlessly mapped to noop 848 | Integer remoteId = myAsyncIdMap.get(id); 849 | if (remoteId != null) { 850 | logMessage(String.format("Compile script done, request %d mapped to %d", remoteId, id)); 851 | } 852 | break; 853 | } 854 | 855 | case RUNTIME_LOG_API: { 856 | // accumulating log API 857 | //{"result":{"result":{"type":"object","objectId":"{\"injectedScriptId\":1,\"id\":5}","className":"Object","description":"Object","preview":{"type":"object","description":"Object","lossless":true,"properties":[{"name":"x","type":"number","value":"0"},{"name":"y","type":"number","value":"79.27999999999997"}]}},"wasThrown":false},"id":36} 858 | myJfxDebuggerAccess.clearArg(); 859 | myRuntimeEvaluateArgResult = json; 860 | logMessage(String.format("Getting Runtime.evaluate param: %s", json.toString())); 861 | return null; 862 | } 863 | 864 | case RUNTIME_LOG_STACK: { 865 | logMessage(String.format("Skipping log stack trace, request %d", id)); 866 | return null; 867 | } 868 | 869 | case RUNTIME_SKIP: { 870 | logMessage(String.format("Skipping request %d, %s", id, param)); 871 | return null; 872 | } 873 | 874 | case DEBUGGER_PAUSED: { 875 | logMessage(String.format("Skipping debug paused request %d, %s", id, param)); 876 | return null; 877 | } 878 | 879 | case BREAK_POINT_REMOVE: { 880 | String toRemove = myBreakpointsToRemove.remove(id); 881 | if (toRemove != null) { 882 | logMessage(String.format("Removing breakpoint request %d, %s", id, param)); 883 | myBreakpoints.remove(toRemove); 884 | } 885 | } 886 | 887 | case REQUEST_JS_BRIDGE: { 888 | if (mySuppressPageReloadRequest) { 889 | mySuppressPageReloadRequest = false; 890 | } else { 891 | // able to debug our jsBridge connection if issuing pause here 892 | if (myDebugOnLoad == DebugOnLoad.ON_BRIDGE_CONNECT) { 893 | myDebugOnLoad = DebugOnLoad.NONE; 894 | debugBreak(null); 895 | } 896 | logMessage(String.format("Skipping response and requesting JSBridge: %s", param)); 897 | myJfxDebuggerAccess.pageReloadStarted(); 898 | return null; 899 | } 900 | } 901 | 902 | case DOM_GET_DOCUMENT: { 903 | // {"result":{"root":{"nodeId":13,"nodeType":9,"nodeName":"#document","localName":"","nodeValue":"","childNodeCount":1,"children":[{"nodeId":14,"nodeType":1,"nodeName":"HTML","localName":"html","nodeValue":"","childNodeCount":1,"children":[{"nodeId":15,"nodeType":1,"nodeName":"HEAD","localName":"head","nodeValue":"","childNodeCount":9,"attributes":[]}],"attributes":[]}],"frameId":"0.1","documentURL":"file:///Users/vlad/src/sites/public/mn-resources/preview_2.html?2","baseURL":"file:///Users/vlad/src/sites/public/mn-resources/preview_2.html?2","xmlVersion":""}},"id":63} 904 | BoxedJsObject jsRoot = json.eval("result.root").asJsObject(); 905 | 906 | if (jsRoot.isValid()) { 907 | BoxedJsArray jsChildren = jsRoot.get("children").asJsArray(); 908 | int parentId = jsRoot.getJsNumber("nodeId").asJsNumber().intValue(); 909 | 910 | logMessage(String.format("Got DOM Root of node: %d", parentId)); 911 | 912 | if (addNodeChildren(parentId, jsChildren, 0, null, 0)) { 913 | logMessage(String.format("Adding DOM Root node: %d", parentId)); 914 | myNodeIdMap.put(parentId, jsRoot); 915 | myRootNodeId = parentId; 916 | } else { 917 | // did not add 918 | logMessage(String.format("Did not add DOM Root node: %d, %s", parentId, param)); 919 | } 920 | } 921 | } 922 | } 923 | } 924 | 925 | // Request: 926 | // {"id":411,"method":"Debugger.setBreakpointByUrl","params":{"lineNumber":16,"url":"file:///Users/vlad/src/sites/public/mn-resources/admonition.js","columnNumber":0,"condition":""}} 927 | // Response: 928 | // {"result":{"breakpointId":"file:///Users/vlad/src/sites/public/mn-resources/admonition.js:16:0","locations":[{"scriptId":"183","lineNumber":16,"columnNumber":0}]},"id":162} 929 | // Request: 930 | // {"id":454,"method":"Debugger.removeBreakpoint","params":{"breakpointId":"file:///Users/vlad/src/sites/public/mn-resources/admonition.js:16:0"}} 931 | // Response: 932 | // {"result":{},"id":177} 933 | 934 | // Request: 935 | // {"id":449,"method":"Debugger.getPossibleBreakpoints","params":{"start":{"scriptId":"220","lineNumber":14,"columnNumber":0},"end":{"scriptId":"220","lineNumber":15,"columnNumber":0},"restrictToFunction":false}} 936 | // Response: Need to send back break points or at least to remove all break points before resuming for disconnect. Otherwise the sucker stops waiting for a response 937 | // {"error":{"code":-32601,"message":"'Debugger.getPossibleBreakpoints' was not found"},"id":174} 938 | // see if break point created, we will remember it 939 | try { 940 | BoxedJsString breakpointId = jsResult.getJsString("breakpointId"); 941 | if (breakpointId.isValid()) { 942 | myBreakpoints.put(breakpointId.getString(), param); 943 | } 944 | } catch (Throwable throwable) { 945 | String message = throwable.getMessage(); 946 | logMessage("Exception on getting breakpointId" + message); 947 | } 948 | 949 | // change id to what the remote expects if we inserted or stripped some calls 950 | Integer remoteId = myAsyncIdMap.remove(id); 951 | if (remoteId != null) { 952 | json.put("id", remoteId); 953 | logMessage(String.format("request %d mapped back to remote %d", id, remoteId)); 954 | changed = true; 955 | } 956 | } 957 | 958 | if (changed) { 959 | changedParam = json.toString(); 960 | } 961 | 962 | if (runOnEvalRunnables && myOnEvalDoneRunnable != null) { 963 | // schedule the next request 964 | String finalChangedParam = changedParam; 965 | final Runnable runnable = myOnEvalDoneRunnable; 966 | myOnEvalDoneRunnable = null; 967 | 968 | yieldDebugger(() -> { 969 | myWaitingForEvaluateScript = false; 970 | runnable.run(); 971 | if (myCallback != null) { 972 | myCallback.call(finalChangedParam); 973 | } 974 | }); 975 | } else { 976 | myWaitingForEvaluateScript = false; 977 | if (myCallback != null) { 978 | myCallback.call(changedParam); 979 | } 980 | } 981 | 982 | return null; 983 | } 984 | 985 | boolean addNodeChildren(int parentId, @NotNull BoxedJsArray jsChildren, int removedNode, @Nullable BoxedJsObject jsInsertedNode, int afterSibling) { 986 | if (jsChildren.isValid()) { 987 | int iMax = jsChildren.size(); 988 | int offset = 0; 989 | int previousNodeId = 0; 990 | 991 | if (jsInsertedNode == null || !jsInsertedNode.isValid()) { 992 | afterSibling = 0; 993 | jsInsertedNode = null; 994 | } 995 | 996 | for (int i = 0; i < iMax; i++) { 997 | BoxedJsObject jsNode = jsChildren.getJsObject(i + offset); 998 | BoxedJsNumber jsNodeId = jsNode.getJsNumber("nodeId"); 999 | if (jsNodeId.isValid()) { 1000 | int nodeId = jsNodeId.intValue(); 1001 | if (removedNode == nodeId) { 1002 | jsChildren.remove(i + offset); 1003 | offset--; 1004 | } else if (jsInsertedNode != null && afterSibling == previousNodeId) { 1005 | jsNode.put("parentId", parentId); 1006 | jsNode.put("ordinal", i + offset); 1007 | 1008 | nodeId = jsInsertedNode.getJsNumber("nodeId").intValue(); 1009 | myNodeIdMap.put(nodeId, jsInsertedNode); 1010 | jsChildren.add(i + offset, jsInsertedNode); 1011 | 1012 | offset++; 1013 | // now recurse to add node's children 1014 | addNodeChildren(nodeId, jsNode.getJsArray("children"), 0, null, 0); 1015 | } else { 1016 | jsNode.put("parentId", parentId); 1017 | jsNode.put("ordinal", i + offset); 1018 | myNodeIdMap.put(nodeId, jsNode); 1019 | 1020 | // now recurse to add node's children 1021 | addNodeChildren(jsNodeId.intValue(), jsNode.getJsArray("children"), 0, null, 0); 1022 | } 1023 | 1024 | previousNodeId = nodeId; 1025 | } 1026 | } 1027 | 1028 | if (jsInsertedNode != null && afterSibling == previousNodeId) { 1029 | BoxedJsObject jsNode = jsInsertedNode; 1030 | BoxedJsNumber jsNodeId = jsNode.get("nodeId").asJsNumber(); 1031 | if (jsNodeId.isValid()) { 1032 | jsNode.put("parentId", parentId); 1033 | jsNode.put("ordinal", iMax + offset); 1034 | myNodeIdMap.put(jsNodeId.intValue(), jsNode); 1035 | 1036 | // now recurse to add node's children 1037 | addNodeChildren(jsNodeId.intValue(), jsNode.getJsArray("children"), 0, null, 0); 1038 | } 1039 | } 1040 | 1041 | BoxedJsObject jsParentParams = myNodeIdMap.get(parentId); 1042 | if (jsParentParams == null) { 1043 | jsParentParams = BoxedJson.boxedFrom(String.format("{\"nodeId\":%d }", parentId)); 1044 | } 1045 | 1046 | // save updated details for the node 1047 | jsParentParams.put("children", jsChildren); 1048 | jsParentParams.put("childCount", jsChildren.size()); 1049 | myNodeIdMap.put(parentId, jsParentParams); 1050 | return true; 1051 | } 1052 | return false; 1053 | } 1054 | 1055 | @Override 1056 | public void sendMessage(final String message) { 1057 | // pre-process messages here and possibly filter/add other messages 1058 | String changedMessage = message; 1059 | BoxedJsObject json = BoxedJson.boxedFrom(message); 1060 | boolean changed = false; 1061 | String evalScript = null; 1062 | 1063 | final BoxedJsString jsonMethod = json.getJsonString("method"); 1064 | final BoxedJsNumber jsId = json.getJsonNumber("id"); 1065 | final int id = jsId.isValid() ? jsId.intValue() : 0; 1066 | 1067 | if (jsonMethod.isValid()) { 1068 | String method = jsonMethod.getString(); 1069 | switch (method) { 1070 | case "Runtime.compileScript": { 1071 | // change to harmless crap and fake the response 1072 | json = BoxedJson.boxedFrom(String.format("{\"id\":%s,\"method\":\"Runtime.enable\"}", jsId.intValue(myDebuggerId))); 1073 | changed = true; 1074 | myAsyncResultMap.put(myDebuggerId, RUNTIME_COMPILE_SCRIPT); 1075 | logMessage(String.format("Faking compileScript, request %d", jsId.intValue())); 1076 | break; 1077 | } 1078 | 1079 | case "Runtime.evaluate": { 1080 | // grab the evaluate expression and eval it so we get the right context for the call 1081 | // then run this and pass it to dev tools 1082 | // {"id":250,"method":"Runtime.evaluate","params":{"expression":"window.__MarkdownNavigatorArgs.getConsoleArg()","objectGroup":"console","includeCommandLineAPI":true,"silent":false,"contextId":4,"returnByValue":false,"generatePreview":true,"userGesture":true,"awaitPromise":false}} 1083 | BoxedJsString jsEvalExpression = json.evalJsString("params.expression"); 1084 | BoxedJsValue jsAwaitPromise = json.evalJsBoolean("params.awaitPromise"); 1085 | BoxedJsValue jsSilent = json.evalJsBoolean("params.silent"); 1086 | BoxedJsValue jsUserGesture = json.evalJsBoolean("params.userGesture"); 1087 | BoxedJsValue jsReturnByValue = json.evalJsBoolean("params.returnByValue"); 1088 | BoxedJsNumber jsContextId = json.evalJsNumber("params.contextId"); 1089 | BoxedJsValue jsIncludeConsoleAPI = json.evalJsBoolean("params.includeCommandLineAPI"); 1090 | 1091 | boolean isPageContext = false; 1092 | if (jsContextId.isValid()) { 1093 | int contextId = jsContextId.intValue(); 1094 | if (myOldPageContextIds.contains(contextId)) { 1095 | contextId = myLastPageContextId; 1096 | } 1097 | isPageContext = contextId == myLastPageContextId; 1098 | } 1099 | 1100 | // lets make sure we can properly execute it 1101 | if (isPageContext 1102 | && jsEvalExpression.isValid() 1103 | && jsSilent.isFalse() 1104 | && jsAwaitPromise.isFalse() 1105 | && jsReturnByValue.isFalse() 1106 | && jsId.isValid() 1107 | ) { 1108 | 1109 | final String evalExpression; 1110 | if (jsIncludeConsoleAPI.isTrue()) { 1111 | String expression = jsEvalExpression.getString(); 1112 | evalExpression = String.format("with (__api) { %s }", expression); 1113 | } else { 1114 | evalExpression = jsEvalExpression.getString(); 1115 | } 1116 | 1117 | // {"result":{"result":{"type":"undefined"},"wasThrown":false},"id":38} 1118 | // {"result":{"result":{"type":"object","objectId":"{\"injectedScriptId\":2,\"id\":116}","className":"Object","description":"Object","preview":{"type":"object","description":"Object","lossless":false,"properties":[{"name":"setEventHandledBy","type":"function","value":""},{"name":"getState","type":"function","value":""},{"name":"setState","type":"function","value":""},{"name":"toggleTask","type":"function","value":""},{"name":"onJsBridge","type":"function","value":""}]}},"wasThrown":false},"id":56} 1119 | Object resultObject = myJfxDebuggerAccess.eval(evalExpression); 1120 | 1121 | // now we get the type for the return result 1122 | BoxedJsObject jsResult = getArgParam(resultObject); 1123 | if (jsResult != null) { 1124 | jsResult.evalSet("id", jsId); 1125 | if (resultObject instanceof JSException) { 1126 | // change message to error 1127 | // {"result":{"result":{"type":"object","objectId":"{\"injectedScriptId\":4,\"id\":350}","subtype":"error","className":"ReferenceError","description":"ReferenceError: Can't find variable: b"},"wasThrown":true},"id":119} 1128 | BoxedJsObject jsResultResult = jsResult.evalJsObject("result.result"); 1129 | jsResultResult.evalSet("subType", "error"); 1130 | String errorMsg = ((JSException) resultObject).getMessage(); 1131 | int pos = errorMsg.indexOf(':'); 1132 | if (pos > 0) { 1133 | jsResultResult.evalSet("className", errorMsg.substring(0, pos)); // set the class name 1134 | } 1135 | jsResultResult.evalSet("description", errorMsg); 1136 | jsResultResult.remove("preview"); 1137 | jsResult.evalSet("result.wasThrown", true); 1138 | } 1139 | // this is what the real debugger responds with when getting an exception in the evaluate 1140 | // {"result":{"result":{"type":"object","objectId":"{\"injectedScriptId\":9,\"id\":123}","subtype":"error","className":"ReferenceError","description":"ReferenceError: Can't find variable: b"},"wasThrown":true},"id":64} 1141 | String param = jsResult.toString(); 1142 | logMessage(String.format("Returning emulated Runtime.evaluate result, request %d: %s", jsId.intValue(), param)); 1143 | // send to dev tools 1144 | if (myCallback != null) { 1145 | myCallback.call(param); 1146 | } 1147 | } 1148 | return; 1149 | } else { 1150 | // execute old code which does not give the right stack frame but won't mess up the debugger either 1151 | myAsyncResultMap.put(myDebuggerId, RUNTIME_EVALUATE_SCRIPT); 1152 | myWaitingForEvaluateScript = true; 1153 | logMessage(String.format("Waiting for evaluateScript, request %d", jsId.intValue())); 1154 | } 1155 | break; 1156 | } 1157 | 1158 | case "Debugger.pause": { 1159 | evalScript = EMPTY_EVAL_SCRIPT; 1160 | myDebuggerState = DebuggerState.PAUSED; 1161 | break; 1162 | } 1163 | 1164 | case "Debugger.stepOver": { 1165 | myDebuggerState = DebuggerState.STEP_OVER; 1166 | break; 1167 | } 1168 | case "Debugger.stepInto": { 1169 | myDebuggerState = DebuggerState.STEP_INTO; 1170 | break; 1171 | } 1172 | case "Debugger.stepOut": { 1173 | myDebuggerState = DebuggerState.STEP_OUT; 1174 | break; 1175 | } 1176 | case "Debugger.resume": { 1177 | myDebuggerState = DebuggerState.RUNNING; 1178 | break; 1179 | } 1180 | case "Page.reload": { 1181 | // need to make sure debugger is not paused 1182 | if (myPageReloadStarted) { 1183 | return; 1184 | } 1185 | myPageReloadStarted = true; 1186 | break; 1187 | } 1188 | 1189 | // FIX: need to figure out what the correct response to this would be 1190 | // probably a list of break point ids 1191 | // Request: 1192 | // {"id":449,"method":"Debugger.getPossibleBreakpoints","params":{"start":{"scriptId":"220","lineNumber":14,"columnNumber":0},"end":{"scriptId":"220","lineNumber":15,"columnNumber":0},"restrictToFunction":false}} 1193 | // Response: Need to send back break points or at least to remove all break points before resuming for disconnect. Otherwise the sucker stops waiting for a response 1194 | // {"error":{"code":-32601,"message":"'Debugger.getPossibleBreakpoints' was not found"},"id":174} 1195 | // however WebView does send this when it parses a script: 1196 | // {"method":"Debugger.breakpointResolved","params":{"breakpointId":"http://localhost/mn-resources/console-test.js:31:0","location":{"scriptId":"288","lineNumber":31,"columnNumber":0}}} 1197 | case "Debugger.setBreakpointByUrl": { 1198 | // nothing to do, we grab all created, however if one already exists the effing debugger responds with an 1199 | // error instead of returning the pre-existing id. Typical Java mindset. 1200 | // Request: 1201 | // {"id":411,"method":"Debugger.setBreakpointByUrl","params":{"lineNumber":16,"url":"file:///Users/vlad/src/sites/public/mn-resources/admonition.js","columnNumber":0,"condition":""}} 1202 | // Response: 1203 | // {"result":{"breakpointId":"file:///Users/vlad/src/sites/public/mn-resources/admonition.js:16:0","locations":[{"scriptId":"183","lineNumber":16,"columnNumber":0}]},"id":162} 1204 | BoxedJsObject params = json.eval("params").asJsObject(); 1205 | BoxedJsNumber jsColumnNumber = params.getJsNumber("columnNumber"); 1206 | BoxedJsNumber jsLineNumber = params.getJsNumber("lineNumber"); 1207 | if (jsLineNumber.isValid() && jsColumnNumber.isValid()) { 1208 | int lineNumber = jsLineNumber.intValue(); 1209 | int columnNumber = jsColumnNumber.intValue(); 1210 | String url = params.get("url").asJsString().getString(); 1211 | if (!url.isEmpty() && lineNumber > 0) { 1212 | String breakPointId = String.format("%s:%d:%d", url, lineNumber, columnNumber); 1213 | for (String key : myBreakpoints.keySet()) { 1214 | if (key.equals(breakPointId)) { 1215 | // this one is it 1216 | BoxedJsObject jsResult = BoxedJson.boxedFrom(myBreakpoints.get(key)); 1217 | jsResult.evalSet("id", json.get("id").asJsNumber()); 1218 | if (myCallback != null) { 1219 | myCallback.call(jsResult.toString()); 1220 | } 1221 | return; 1222 | } 1223 | } 1224 | } 1225 | } 1226 | break; 1227 | } 1228 | 1229 | case "Debugger.removeBreakpoint": { 1230 | // Request: 1231 | // {"id":454,"method":"Debugger.removeBreakpoint","params":{"breakpointId":"file:///Users/vlad/src/sites/public/mn-resources/admonition.js:16:0"}} 1232 | // Response: 1233 | // {"result":{},"id":177} 1234 | BoxedJsString jsBreakpointId = json.eval("params.breakpointId").asJsString(); 1235 | if (jsBreakpointId.isValid()) { 1236 | // we will remove it on the response, if it is in out list 1237 | if (myBreakpoints.containsKey(jsBreakpointId.getString())) { 1238 | myAsyncResultMap.put(myDebuggerId, BREAK_POINT_REMOVE); 1239 | myBreakpointsToRemove.put(myDebuggerId, jsBreakpointId.getString()); 1240 | } 1241 | } 1242 | break; 1243 | } 1244 | 1245 | case "DOM.getDocument": { 1246 | // Request: 1247 | // {"id":63,"method":"DOM.getDocument"} 1248 | // Response: 1249 | // {"result":{"root":{"nodeId":13,"nodeType":9,"nodeName":"#document","localName":"","nodeValue":"","childNodeCount":1,"children":[{"nodeId":14,"nodeType":1,"nodeName":"HTML","localName":"html","nodeValue":"","childNodeCount":1,"children":[{"nodeId":15,"nodeType":1,"nodeName":"HEAD","localName":"head","nodeValue":"","childNodeCount":9,"attributes":[]}],"attributes":[]}],"frameId":"0.1","documentURL":"file:///Users/vlad/src/sites/public/mn-resources/preview_2.html?2","baseURL":"file:///Users/vlad/src/sites/public/mn-resources/preview_2.html?2","xmlVersion":""}},"id":63} 1250 | myAsyncResultMap.put(myDebuggerId, DOM_GET_DOCUMENT); 1251 | break; 1252 | } 1253 | 1254 | case "Overlay.setPausedInDebuggerMessage": { 1255 | // Request: 1256 | // {"id":108,"method":"Overlay.setPausedInDebuggerMessage"} 1257 | // Response: 1258 | // {"result":{},"id":177} 1259 | if (myDebugger != null && myDebugger.isEnabled() && jsId.isValid()) { 1260 | int responseId = jsId.intValue(); 1261 | yieldDebugger(() -> { 1262 | call(String.format("{\"result\":{},\"id\":%d}", responseId)); 1263 | // we now figure out the selector and invoke js helper 1264 | //myJfxDebuggerAccess.eval(""); 1265 | }); 1266 | } 1267 | 1268 | return; 1269 | } 1270 | 1271 | case "Overlay.highlightNode": { 1272 | // Request: 1273 | // {"id":43,"method":"Overlay.highlightNode","params":{"highlightConfig":{"showInfo":true,"showRulers":false,"showExtensionLines":false,"contentColor":{"r":111,"g":168,"b":220,"a":0.66},"paddingColor":{"r":147,"g":196,"b":125,"a":0.55},"borderColor":{"r":255,"g":229,"b":153,"a":0.66},"marginColor":{"r":246,"g":178,"b":107,"a":0.66},"eventTargetColor":{"r":255,"g":196,"b":196,"a":0.66},"shapeColor":{"r":96,"g":82,"b":177,"a":0.8},"shapeMarginColor":{"r":96,"g":82,"b":127,"a":0.6},"displayAsMaterial":true,"cssGridColor":{"r":75,"g":0,"b":130}},"nodeId":2}} 1274 | // Response: 1275 | // {"result":{},"id":177} 1276 | if (myDebugger != null && myDebugger.isEnabled() && jsId.isValid()) { 1277 | int responseId = jsId.intValue(); 1278 | BoxedJsObject jsParams = json.get("params").asJsObject(); 1279 | BoxedJsNumber jsNodeId = jsParams.getJsonNumber("nodeId"); 1280 | if (jsNodeId.isValid()) { 1281 | // build path based on index of child in parent 1282 | StringBuilder sb = new StringBuilder(); 1283 | sb.append("[0"); 1284 | addNodeSelectorPath(jsNodeId.intValue(), sb); 1285 | sb.append(']'); 1286 | String nodeSelectorPath = sb.toString(); 1287 | 1288 | // validate that we get the same node after traversing the path from document 1289 | if (nodeSelectorPath.length() >= 5) { 1290 | // print out the nodes in the path 1291 | String path = nodeSelectorPath.substring(3, nodeSelectorPath.length() - 1); 1292 | String[] parts = path.split(","); 1293 | 1294 | int iMax = parts.length; 1295 | int nodeId = myRootNodeId; 1296 | 1297 | for (int i = iMax; i-- > 0; ) { 1298 | int index = Integer.parseInt(parts[i]); 1299 | BoxedJsObject jsParentParams = myNodeIdMap.get(nodeId); 1300 | if (jsParentParams != null && jsParentParams.isValid()) { 1301 | BoxedJsArray jsChildren = jsParentParams.getJsonArray("children"); 1302 | if (jsChildren.isValid()) { 1303 | BoxedJsObject jsNode = jsChildren.getJsonObject(index); 1304 | if (jsNode.isValid() && jsNode.getJsonNumber("nodeId").isValid()) { 1305 | nodeId = jsNode.getInt("nodeId"); 1306 | } else { 1307 | logMessage(String.format("Invalid child at %d for node %d: %s", index, nodeId, jsParentParams)); 1308 | } 1309 | } else { 1310 | logMessage(String.format("Invalid node children for %d: %s", nodeId, jsParentParams)); 1311 | } 1312 | } else { 1313 | logMessage(String.format("No node information for %d in path %s", nodeId, path)); 1314 | } 1315 | } 1316 | 1317 | if (nodeId != jsNodeId.intValue()) { 1318 | logMessage(String.format("Wrong node %d for requested %d from path %s", nodeId, jsNodeId.intValue(), path)); 1319 | } 1320 | 1321 | yieldDebugger(() -> { 1322 | call(String.format("{\"result\":{},\"id\":%d}", responseId)); 1323 | // we now figure out the selector and invoke js helper 1324 | BoxedJsObject paramJson = argParamJson(); 1325 | // {"id":250,"method":"Runtime.evaluate","params":{"expression":"window.__MarkdownNavigatorArgs.getConsoleArg()","objectGroup":"console","includeCommandLineAPI":true,"silent":false,"contextId":4,"returnByValue":false,"generatePreview":true,"userGesture":true,"awaitPromise":false}} 1326 | //final String argScript = "window.injectedCode = { tests: () => { return \"I'm injected\"; } };"; 1327 | final String argScript = "markdownNavigator.highlightNode(" + nodeSelectorPath + ")"; 1328 | paramJson.evalSet("id", myDebuggerId) 1329 | .evalSet("params.expression", argScript) 1330 | .evalSet("params.includeCommandLineAPI", true) 1331 | .evalSet("params.objectGroup", "console") 1332 | .evalSet("params.userGesture", false) 1333 | .evalSet("params.generatePreview", false) 1334 | .evalSet("params.contextId", myLastPageContextId) 1335 | ; 1336 | 1337 | logMessage(String.format("invoking highlightNode helper nodeId %d, request %d", jsNodeId.intValue(), myDebuggerId)); 1338 | 1339 | myAsyncResultMap.put(myDebuggerId, RUNTIME_SKIP); 1340 | myDebuggerId++; 1341 | debuggerSend(paramJson.toString(), null); 1342 | }); 1343 | } else if (!nodeSelectorPath.equals("[0]")) { 1344 | logMessage(String.format("Invalid path %s, for nodeId %d", nodeSelectorPath, jsNodeId.intValue())); 1345 | } 1346 | } 1347 | } 1348 | 1349 | return; 1350 | } 1351 | 1352 | case "Overlay.hideHighlight": { 1353 | // Request: 1354 | // {"id":64,"method":"Overlay.hideHighlight"} 1355 | // Response: 1356 | // {"result":{},"id":177} 1357 | if (myDebugger != null && myDebugger.isEnabled() && jsId.isValid()) { 1358 | int responseId = jsId.intValue(); 1359 | yieldDebugger(() -> { 1360 | call(String.format("{\"result\":{},\"id\":%d}", responseId)); 1361 | // call jsHelper for that 1362 | BoxedJsObject paramJson = argParamJson(); 1363 | //final String argScript = "window.injectedCode = { tests: () => { return \"I'm injected\"; } };"; 1364 | final String argScript = "markdownNavigator.hideHighlight()"; 1365 | paramJson.evalSet("id", myDebuggerId) 1366 | .evalSet("params.expression", argScript) 1367 | .evalSet("params.includeCommandLineAPI", true) 1368 | .evalSet("params.objectGroup", "console") 1369 | .evalSet("params.userGesture", false) 1370 | .evalSet("params.generatePreview", false) 1371 | .evalSet("params.contextId", myLastPageContextId) 1372 | ; 1373 | 1374 | logMessage(String.format("invoking setHideHighlight helper, request %d", myDebuggerId)); 1375 | myAsyncResultMap.put(myDebuggerId, RUNTIME_SKIP); 1376 | myDebuggerId++; 1377 | debuggerSend(paramJson.toString(), null); 1378 | }); 1379 | } 1380 | 1381 | return; 1382 | } 1383 | } 1384 | } 1385 | 1386 | if (id > 0) { 1387 | if (id != myDebuggerId) { 1388 | // need to change ids 1389 | json.put("id", myDebuggerId); 1390 | changed = true; 1391 | 1392 | // will need to re-map id on result when it is ready 1393 | myAsyncIdMap.put(myDebuggerId, id); 1394 | logMessage(String.format("Mapping request %d to %d", id, myDebuggerId)); 1395 | } 1396 | 1397 | myDebuggerId++; 1398 | } 1399 | 1400 | if (!myOldPageContextIds.isEmpty()) { 1401 | // change old page context id to latest 1402 | String contextIdPath = "params.contextId"; 1403 | BoxedJsNumber contextIdNumber = json.eval(contextIdPath).asJsNumber(); 1404 | Integer contextId = contextIdNumber.isValid() ? contextIdNumber.intValue() : null; 1405 | 1406 | if (contextId == null) { 1407 | contextIdPath = "params.executionContextId"; 1408 | contextIdNumber = json.eval(contextIdPath).asJsNumber(); 1409 | contextId = contextIdNumber.isValid() ? contextIdNumber.intValue() : null; 1410 | } 1411 | 1412 | if (contextId != null && myOldPageContextIds.contains(contextId)) { 1413 | json.evalSet(contextIdPath, myLastPageContextId); 1414 | logMessage(String.format("Mapping old context id %d to %d", contextId, myLastPageContextId)); 1415 | changed = true; 1416 | } 1417 | } 1418 | 1419 | if (changed) { 1420 | changedMessage = json.toString(); 1421 | } 1422 | 1423 | if (myDebugger != null && myDebugger.isEnabled()) { 1424 | debuggerSend(changedMessage, null); 1425 | if (evalScript != null) { 1426 | myJfxDebuggerAccess.eval(evalScript); 1427 | } 1428 | } 1429 | } 1430 | 1431 | void addNodeSelectorPath(int nodeId, StringBuilder out) { 1432 | if (nodeId != myRootNodeId) { 1433 | BoxedJsObject jsNode = myNodeIdMap.getOrDefault(nodeId, BoxedJsValue.HAD_NULL_OBJECT); 1434 | if (jsNode.isValid()) { 1435 | // add node selector to list, these are reverse order 1436 | // format is: ,ordinal in parent 1437 | BoxedJsNumber jsParentId = jsNode.get("parentId").asJsNumber(); 1438 | BoxedJsNumber jsOrdinal = jsNode.get("ordinal").asJsNumber(); 1439 | if (jsParentId.isValid() && jsOrdinal.isValid()) { 1440 | out.append(',').append(jsOrdinal.intValue()); 1441 | addNodeSelectorPath(jsParentId.intValue(), out); 1442 | } 1443 | } 1444 | } 1445 | } 1446 | 1447 | @Override 1448 | public Callback getMessageCallback() { 1449 | return myCallback; 1450 | } 1451 | 1452 | @Override 1453 | public void setMessageCallback(final Callback callback) { 1454 | myCallback = callback; 1455 | } 1456 | 1457 | @Override 1458 | public boolean isEnabled() { 1459 | myIsEnabled = myDebugger != null && myDebugger.isEnabled(); 1460 | return myIsEnabled; 1461 | } 1462 | 1463 | @Override 1464 | public void setEnabled(final boolean enabled) { 1465 | if (myDebugger != null) { 1466 | myIsEnabled = enabled; 1467 | myDebugger.setEnabled(myIsEnabled); 1468 | } 1469 | } 1470 | } 1471 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/DevToolsDebuggerJsBridge.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import com.vladsch.boxed.json.BoxedJsObject; 29 | import com.vladsch.boxed.json.BoxedJsValue; 30 | import com.vladsch.boxed.json.BoxedJson; 31 | import javafx.application.Platform; 32 | import javafx.scene.web.WebEngine; 33 | import javafx.scene.web.WebView; 34 | import netscape.javascript.JSException; 35 | import netscape.javascript.JSObject; 36 | import org.jetbrains.annotations.NotNull; 37 | import org.jetbrains.annotations.Nullable; 38 | 39 | import javax.json.JsonValue; 40 | import java.io.IOException; 41 | import java.io.InputStream; 42 | import java.io.InputStreamReader; 43 | import java.io.StringWriter; 44 | import java.util.Map; 45 | import java.util.function.Consumer; 46 | 47 | public class DevToolsDebuggerJsBridge { 48 | final @NotNull JfxDebuggerAccess myJfxDebuggerAccess; 49 | final @NotNull JfxScriptArgAccessor myJfxScriptArgAccessor; 50 | final @NotNull JfxDebugProxyJsBridge myJfxDebugProxyJsBridge; 51 | final @NotNull WebView myWebView; 52 | final @NotNull DevToolsDebugProxy myDebugger; 53 | final @Nullable JfxScriptStateProvider myStateProvider; 54 | @Nullable String myJSEventHandledBy; 55 | final LogHandler LOG = LogHandler.getInstance(); 56 | 57 | private final long myNanos = System.nanoTime(); 58 | private final long myMilliNanos = System.currentTimeMillis() * 1000000; 59 | @Nullable Object myConsoleArg = null; 60 | @Nullable Object myArg = null; 61 | @Nullable DevToolsDebuggerServer myDebuggerServer; 62 | final int myInstance; 63 | final boolean mySuppressNoMarkdownException; 64 | 65 | public DevToolsDebuggerJsBridge(@NotNull final WebView webView, final @NotNull WebEngine engine, int instance, @Nullable JfxScriptStateProvider stateProvider) { 66 | this(webView, engine, instance, stateProvider, false); 67 | } 68 | 69 | public DevToolsDebuggerJsBridge(@NotNull final WebView webView, final @NotNull WebEngine engine, int instance, @Nullable JfxScriptStateProvider stateProvider, boolean suppressNoMarkdownException) { 70 | myWebView = webView; 71 | myInstance = instance; 72 | myStateProvider = stateProvider; 73 | myJfxDebuggerAccess = new JfxDebuggerAccessImpl(); 74 | myJfxScriptArgAccessor = new JfxScriptArgAccessorDelegate(new JfxScriptArgAccessorImpl()); 75 | myJfxDebugProxyJsBridge = new JfxDebugProxyJsBridgeDelegate(new JfxDebugProxyJsBridgeImpl()); 76 | myDebugger = new DevToolsDebugProxy(engine, myJfxDebuggerAccess); 77 | mySuppressNoMarkdownException = suppressNoMarkdownException; 78 | } 79 | 80 | protected @NotNull JfxDebugProxyJsBridge getJfxDebugProxyJsBridge() { 81 | return myJfxDebugProxyJsBridge; 82 | } 83 | 84 | /** 85 | * Called when page reload instigated 86 | *

87 | * Will not be invoked if {@link #pageReloading()} is called before invoking reload on WebView engine side 88 | *

89 | * Will always be invoked if page reload requested by Chrome dev tools 90 | *

91 | * Override in a subclass to get notified of this event 92 | */ 93 | protected void pageReloadStarted() { 94 | 95 | } 96 | 97 | /** 98 | * Inject JSBridge and initialize the JavaScript helper 99 | *

100 | * Call when engine transitions state to READY after a page load is started in WebView or requested 101 | * by Chrome dev tools. 102 | *

103 | * This means after either the {@link #pageReloading()} is called or {@link #pageReloadStarted()} 104 | * is invoked to inform of page reloading operation. 105 | */ 106 | public void connectJsBridge() { 107 | JSObject jsObject = (JSObject) myWebView.getEngine().executeScript("window"); 108 | jsObject.setMember("__MarkdownNavigatorArgs", myJfxScriptArgAccessor); // this interface stays for the duration, does not give much 109 | jsObject.setMember("__MarkdownNavigator", getJfxDebugProxyJsBridge()); // this interface is captured by the helper script since incorrect use can bring down the whole app 110 | try { 111 | if (mySuppressNoMarkdownException) { 112 | myWebView.getEngine().executeScript("var markdownNavigator; markdownNavigator && markdownNavigator.setJsBridge(window.__MarkdownNavigator);"); 113 | } else { 114 | myWebView.getEngine().executeScript("markdownNavigator.setJsBridge(window.__MarkdownNavigator);"); 115 | } 116 | } catch (JSException e) { 117 | e.printStackTrace(); 118 | LOG.warn("jsBridgeHelperScript: exception", e); 119 | } 120 | jsObject.removeMember("__MarkdownNavigator"); 121 | } 122 | 123 | /** 124 | * InputStream for the JSBridge Helper script to inject during page loading 125 | *

126 | * Override if you want to customize the jsBridge helper script 127 | * 128 | * @return input stream 129 | */ 130 | public InputStream getJsBridgeHelperAsStream() { 131 | return DevToolsDebuggerJsBridge.class.getResourceAsStream("/markdown-navigator.js"); 132 | } 133 | 134 | /** 135 | * InputStream for the JSBridge Helper script to inject during page loading 136 | *

137 | * Override if you want to customize the jsBridge helper script 138 | * 139 | * @return input stream 140 | */ 141 | public String getJsBridgeHelperURL() { 142 | return String.valueOf(DevToolsDebuggerJsBridge.class.getResource("/markdown-navigator.js")); 143 | } 144 | 145 | /** 146 | * Called before standard helper script is written 147 | * 148 | * @param writer where to add prefix script if one is desired 149 | */ 150 | protected void jsBridgeHelperScriptSuffix(final StringWriter writer) { 151 | 152 | } 153 | 154 | /** 155 | * Called after standard helper script is written 156 | * 157 | * @param writer where to add suffix script if one is desired 158 | */ 159 | protected void jsBridgeHelperScriptPrefix(final StringWriter writer) { 160 | 161 | } 162 | 163 | /** 164 | * Call to signal to debug server that a page is about to be reloaded 165 | *

166 | * Should be called only if page reloads triggered from WebView side should 167 | * not generate call backs on pageReloadStarted() 168 | */ 169 | public void pageReloading() { 170 | if (myDebuggerServer != null) { 171 | myDebuggerServer.pageReloading(); 172 | } 173 | } 174 | 175 | /** 176 | * Called by JavaScript helper to signal all script operations are complete 177 | *

178 | * Used to prevent rapid page reload requests from Chrome dev tools which has 179 | * a way of crashing the application with a core dump on Mac and probably the same 180 | * on other OS implementations. 181 | *

182 | * Override if need to be informed of this event 183 | */ 184 | protected void pageLoadComplete() { 185 | 186 | } 187 | 188 | /** 189 | * Used to get the handled by JavaScript handler 190 | *

191 | * This is needed to allow js to signal that an event has been handled. Java registered 192 | * event listeners ignore the JavaScript event.stopPropagation() and event.preventDefault() 193 | *

194 | * To use this properly clear this field after getting its value in the event listener. 195 | * 196 | * @return string passed by to {@link JfxDebugProxyJsBridge#setEventHandledBy(String)} by JavaScript 197 | */ 198 | public @Nullable String getJSEventHandledBy() { 199 | return myJSEventHandledBy; 200 | } 201 | 202 | /** 203 | * Used to clear the handled by JavaScript handler 204 | *

205 | * This is needed to allow js to signal that an event has been handled. Java registered 206 | * event listeners ignore the JavaScript event.stopPropagation() and event.preventDefault() 207 | *

208 | * To use this properly clear this field after getting its value in the event listener. 209 | */ 210 | public void clearJSEventHandledBy() { 211 | myJSEventHandledBy = null; 212 | } 213 | 214 | /** 215 | * Launch a debugger instance 216 | * 217 | * @param port debugger port 218 | * @param onFailure call back on failure 219 | * @param onStart call back on success 220 | */ 221 | public void startDebugServer(final int port, @Nullable Consumer onFailure, @Nullable Runnable onStart) { 222 | if (myDebuggerServer == null) { 223 | Platform.runLater(() -> { 224 | DevToolsDebuggerServer[] debuggerServer = new DevToolsDebuggerServer[] { null }; 225 | 226 | debuggerServer[0] = new DevToolsDebuggerServer(myDebugger, port, myInstance, throwable -> { 227 | myDebuggerServer = null; 228 | if (onFailure != null) { 229 | onFailure.accept(throwable); 230 | } 231 | }, () -> { 232 | myDebuggerServer = debuggerServer[0]; 233 | if (onStart != null) { 234 | onStart.run(); 235 | } 236 | }); 237 | }); 238 | } else { 239 | if (onStart != null) { 240 | onStart.run(); 241 | } 242 | } 243 | } 244 | 245 | /** 246 | * Stop debugger 247 | *

248 | * Server is not accessible after this call even before the onStopped callback is invoked. 249 | * 250 | * @param onStopped call back to execute when the server stops, parameter true if server shut down, false if only disconnected (other instances running), null was not connected to debug server 251 | */ 252 | public void stopDebugServer(@Nullable Consumer onStopped) { 253 | if (myDebuggerServer != null) { 254 | DevToolsDebuggerServer debuggerServer = myDebuggerServer; 255 | myDebuggerServer = null; 256 | debuggerServer.stopDebugServer((shutdown) -> { 257 | if (onStopped != null) { 258 | onStopped.accept(shutdown); 259 | } 260 | }); 261 | } else { 262 | if (onStopped != null) { 263 | onStopped.accept(null); 264 | } 265 | } 266 | } 267 | 268 | public @NotNull String getDebuggerURL() { 269 | return myDebuggerServer != null ? myDebuggerServer.getDebugUrl() : ""; 270 | } 271 | 272 | /** 273 | * State of debugger 274 | * 275 | * @return true if debugger is enabled but not necessarily connected 276 | */ 277 | public boolean isDebuggerEnabled() { 278 | return myDebuggerServer != null; 279 | } 280 | 281 | /** 282 | * State of debugger 283 | * 284 | * @return true if actively debugging, ie. debugger is connected 285 | */ 286 | public boolean isDebugging() { 287 | return isDebuggerEnabled() && myDebuggerServer != null && myDebuggerServer.isDebuggerConnected(); 288 | } 289 | 290 | /** 291 | * Reload page with option to pause debugger on load 292 | * 293 | * @param breakOnLoad true if debugger is to pause before JSBridge is connected 294 | * @param debugBreakInjectionOnLoad true if debugger is to pause before any scripts execute and before the JSBridge helper script is executed. 295 | */ 296 | public void reloadPage(boolean breakOnLoad, boolean debugBreakInjectionOnLoad) { 297 | if ((breakOnLoad || debugBreakInjectionOnLoad) && this.isDebuggerEnabled() && myDebuggerServer != null && myDebuggerServer.isDebuggerConnected()) { 298 | myDebuggerServer.setDebugOnLoad(debugBreakInjectionOnLoad ? JfxDebuggerProxy.DebugOnLoad.ON_INJECT_HELPER : JfxDebuggerProxy.DebugOnLoad.ON_BRIDGE_CONNECT); 299 | myDebuggerServer.reloadPage(); 300 | } else { 301 | Platform.runLater(() -> { 302 | myWebView.getEngine().reload(); 303 | }); 304 | } 305 | } 306 | 307 | public void onConnectionOpen() { 308 | 309 | } 310 | 311 | public void onConnectionClosed() { 312 | 313 | } 314 | 315 | /** 316 | * Interface implementation for proxy to get access and do callbacks 317 | */ 318 | private class JfxDebuggerAccessImpl implements JfxDebuggerAccess { 319 | JfxDebuggerAccessImpl() {} 320 | 321 | @Override 322 | public void onConnectionOpen() { 323 | DevToolsDebuggerJsBridge.this.onConnectionOpen(); 324 | } 325 | 326 | @Override 327 | public void onConnectionClosed() { 328 | DevToolsDebuggerJsBridge.this.onConnectionClosed(); 329 | } 330 | 331 | @Override 332 | public String setArg(final Object arg) { 333 | myConsoleArg = arg; 334 | return "window.__MarkdownNavigatorArgs.getConsoleArg()"; 335 | } 336 | 337 | @Override 338 | public void clearArg() { 339 | myConsoleArg = null; 340 | } 341 | 342 | @Override 343 | public Object eval(final String script) { 344 | return myWebView.getEngine().executeScript(script); 345 | } 346 | 347 | @Override 348 | public void pageReloadStarted() { 349 | DevToolsDebuggerJsBridge.this.pageReloadStarted(); 350 | } 351 | 352 | @Override 353 | public String jsBridgeHelperScript() { 354 | StringWriter writer = new StringWriter(); 355 | InputStream inputStream = getJsBridgeHelperAsStream(); 356 | InputStreamReader reader = new InputStreamReader(inputStream); 357 | 358 | writer.append("var markdownNavigator;"); 359 | 360 | DevToolsDebuggerJsBridge.this.jsBridgeHelperScriptPrefix(writer); 361 | 362 | try { 363 | char[] buffer = new char[4096]; 364 | int n; 365 | while (-1 != (n = reader.read(buffer))) { 366 | writer.write(buffer, 0, n); 367 | } 368 | reader.close(); 369 | inputStream.close(); 370 | } catch (IOException e) { 371 | LOG.error("jsBridgeHelperScript: exception", e); 372 | } 373 | 374 | DevToolsDebuggerJsBridge.this.jsBridgeHelperScriptSuffix(writer); 375 | 376 | // log in the injection script and with debug break on load seems to be unstable 377 | //writer.append('\n') 378 | //writer.append("console.log(\"markdownNavigator: %cInjected\", \"color: #bb002f\");") 379 | 380 | appendStateString(writer); 381 | return writer.toString(); 382 | } 383 | } 384 | 385 | /** 386 | * Appends the java script code to initialize the persistent state. 387 | * Adding this in <script> tags will allow scripts to get access 388 | * to their previously saved state before JSBridge is established. 389 | *

390 | * Should be used at the top of the body to 391 | * 392 | * @param sb appendable 393 | */ 394 | public void appendStateString(Appendable sb) { 395 | // output all the state vars 396 | if (myStateProvider != null) { 397 | try { 398 | for (Map.Entry entry : myStateProvider.getState().entrySet()) { 399 | sb.append("markdownNavigator.setState(\"").append(entry.getKey()).append("\", ").append(entry.getValue().toString()).append(");\n"); 400 | } 401 | } catch (IOException e) { 402 | LOG.error("appendStateString: exception", e); 403 | } 404 | } 405 | } 406 | 407 | /** 408 | * The java script code to initialize the persistent state. 409 | * Adding this in <script> tags will allow scripts to get access 410 | * to their previously saved state before JSBridge is established. 411 | *

412 | * Should be used at the top of the body to 413 | * 414 | * @return text of the state initialization to be included in the HTML page between <script> tags. 415 | */ 416 | public String getStateString() { 417 | // output all the state vars 418 | StringBuilder sb = new StringBuilder(); 419 | appendStateString(sb); 420 | return sb.toString(); 421 | } 422 | 423 | private class JfxScriptArgAccessorImpl implements JfxScriptArgAccessor { 424 | JfxScriptArgAccessorImpl() {} 425 | 426 | @Override 427 | public @Nullable Object getArg() { 428 | return myArg; 429 | } 430 | 431 | @Override 432 | public @Nullable Object getConsoleArg() { 433 | return myConsoleArg; 434 | } 435 | } 436 | 437 | long timestamp() { 438 | return (System.nanoTime() - myNanos) + myMilliNanos; 439 | } 440 | 441 | /* 442 | * Interface implementation for Call backs from JavaScript used by the JSBridge helper 443 | * script markdown-navigator.js 444 | */ 445 | private class JfxDebugProxyJsBridgeImpl implements JfxDebugProxyJsBridge { 446 | JfxDebugProxyJsBridgeImpl() {} 447 | 448 | /** 449 | * Called by JavaScript helper to signal all script operations are complete 450 | *

451 | * Used to prevent rapid page reload requests from Chrome dev tools which has 452 | * a way of crashing the application with a core dump on Mac and probably the same 453 | * on other OS implementations. 454 | *

455 | * Override if need to be informed of this event but make sure super.pageLoadComplete() 456 | * is called. 457 | */ 458 | @Override 459 | public void pageLoadComplete() { 460 | if (myDebuggerServer != null) { 461 | myDebuggerServer.pageLoadComplete(); 462 | } 463 | DevToolsDebuggerJsBridge.this.pageLoadComplete(); 464 | } 465 | 466 | @Override 467 | public void consoleLog(final @NotNull String type, final @NotNull JSObject args) { 468 | try { 469 | long timestamp = timestamp(); 470 | // funnel it to the debugger console 471 | if (myDebuggerServer != null) { 472 | myDebuggerServer.log(type, timestamp, args); 473 | } 474 | } catch (Throwable e) { 475 | LOG.debug(String.format("[%d] Exception in consoleLog: ", myInstance)); 476 | LOG.error(e); 477 | } 478 | } 479 | 480 | @Override 481 | public void println(final @Nullable String text) { 482 | System.out.println(text == null ? "null" : text); 483 | } 484 | 485 | @Override 486 | public void print(final @Nullable String text) { 487 | System.out.println(text == null ? "null" : text); 488 | } 489 | 490 | @Override 491 | public void setEventHandledBy(final String handledBy) { 492 | myJSEventHandledBy = handledBy; 493 | } 494 | 495 | @Override 496 | public @Nullable Object getState(final String name) { 497 | // convert to JSObject 498 | if (myStateProvider != null) { 499 | BoxedJsValue state = myStateProvider.getState().get(name); 500 | if (state.isValid()) { 501 | return myWebView.getEngine().executeScript("(" + state.toString() + ")"); 502 | } 503 | } 504 | return null; 505 | } 506 | 507 | @Override 508 | public void setState(final @NotNull String name, final @Nullable Object state) { 509 | if (myStateProvider != null) { 510 | BoxedJsObject scriptState = myStateProvider.getState(); 511 | scriptState.remove(name); 512 | 513 | if (state != null) { 514 | // convert JSObject to Element others to attributes 515 | try { 516 | BoxedJsValue value = null; 517 | if (state instanceof Integer) value = BoxedJson.boxedOf((int) state); 518 | else if (state instanceof Boolean) value = BoxedJson.boxedOf((boolean) state); 519 | else if (state instanceof Double) value = BoxedJson.boxedOf((double) state); 520 | else if (state instanceof Float) value = BoxedJson.boxedOf((float) state); 521 | else if (state instanceof Long) value = BoxedJson.boxedOf((long) state); 522 | else if (state instanceof Character) value = BoxedJson.boxedOf(String.valueOf((char) state)); 523 | else if (state instanceof String) value = BoxedJson.boxedOf((String) state); 524 | else if (state instanceof JSObject) { 525 | // need to convert to JSON string 526 | myArg = state; 527 | String jsonString = (String) myWebView.getEngine().executeScript("JSON.stringify(window.__MarkdownNavigatorArgs.getArg(), null, 0)"); 528 | myArg = null; 529 | 530 | value = BoxedJson.boxedFrom(jsonString); 531 | } 532 | if (value != null && value.isValid()) { 533 | scriptState.put(name, value); 534 | } 535 | } catch (Throwable e) { 536 | LOG.error(e); 537 | } 538 | } 539 | 540 | // strictly a formality since the state is mutable, but lets provider save/cache or whatever else 541 | myStateProvider.setState(scriptState); 542 | } 543 | } 544 | 545 | /** 546 | * Callback from JavaScript to initiate a debugger pause 547 | */ 548 | public void debugBreak() { 549 | if (myDebuggerServer != null) { 550 | myDebuggerServer.debugBreak(); 551 | } 552 | } 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/DevToolsDebuggerServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import com.sun.javafx.scene.web.Debugger; 29 | import javafx.application.Platform; 30 | import netscape.javascript.JSObject; 31 | import org.jetbrains.annotations.NotNull; 32 | import org.jetbrains.annotations.Nullable; 33 | 34 | import java.net.InetSocketAddress; 35 | import java.nio.channels.NotYetConnectedException; 36 | import java.util.HashMap; 37 | import java.util.function.Consumer; 38 | 39 | public class DevToolsDebuggerServer implements JfxDebuggerConnector { 40 | final static HashMap ourServerMap = new HashMap<>(); 41 | 42 | final Debugger myDebugger; 43 | JfxWebSocketServer myServer; 44 | final LogHandler LOG = LogHandler.getInstance(); 45 | 46 | public DevToolsDebuggerServer(@NotNull Debugger debugger, int debuggerPort, final int instanceId, @Nullable Consumer onFailure, @Nullable Runnable onStart) { 47 | myDebugger = debugger; 48 | boolean freshStart = false; 49 | 50 | JfxWebSocketServer jfxWebSocketServer; 51 | 52 | synchronized (ourServerMap) { 53 | jfxWebSocketServer = ourServerMap.get(debuggerPort); 54 | 55 | if (jfxWebSocketServer == null) { 56 | jfxWebSocketServer = new JfxWebSocketServer(new InetSocketAddress("localhost", debuggerPort), (ex) -> { 57 | synchronized (ourServerMap) { 58 | if (ourServerMap.get(debuggerPort).removeServer(this)) { 59 | // unused 60 | ourServerMap.remove(debuggerPort); 61 | } 62 | } 63 | 64 | if (onFailure != null) { 65 | onFailure.accept(ex); 66 | } 67 | }, (server) -> { 68 | myServer = server; 69 | initDebugger(instanceId, onStart); 70 | }); 71 | 72 | ourServerMap.put(debuggerPort, jfxWebSocketServer); 73 | freshStart = true; 74 | } 75 | } 76 | 77 | jfxWebSocketServer.addServer(this, instanceId); 78 | 79 | if (freshStart) { 80 | jfxWebSocketServer.start(); 81 | } else { 82 | myServer = jfxWebSocketServer; 83 | initDebugger(instanceId, onStart); 84 | } 85 | } 86 | 87 | private void initDebugger(int instanceId, @Nullable Runnable onStart) { 88 | Platform.runLater(() -> { 89 | myDebugger.setEnabled(true); 90 | myDebugger.sendMessage("{\"id\" : -1, \"method\" : \"Network.enable\"}"); 91 | 92 | this.myDebugger.setMessageCallback(data -> { 93 | try { 94 | myServer.send(this, data); 95 | } catch (NotYetConnectedException e) { 96 | e.printStackTrace(); 97 | } 98 | return null; 99 | }); 100 | 101 | if (LOG.isDebugEnabled()) { 102 | String remoteUrl = getDebugUrl(); 103 | System.out.println("Debug session created. Debug URL: " + remoteUrl); 104 | LOG.debug("Debug session created. Debug URL: " + remoteUrl); 105 | } 106 | 107 | if (onStart != null) { 108 | onStart.run(); 109 | } 110 | }); 111 | } 112 | 113 | public boolean isDebuggerConnected() { 114 | return myServer.isDebuggerConnected(this); 115 | } 116 | 117 | @NotNull 118 | public String getDebugUrl() { 119 | // Chrome won't launch first session if we have a query string 120 | return myServer != null ? myServer.getDebugUrl(this) : ""; 121 | } 122 | 123 | public void stopDebugServer(@NotNull Consumer onStopped) { 124 | if (myServer != null) { 125 | Platform.runLater(() -> { 126 | Runnable action = () -> { 127 | if (LOG.isDebugEnabled()) { 128 | String remoteUrl = getDebugUrl(); 129 | System.out.println("Debug session stopped for URL: " + remoteUrl); 130 | LOG.debug("Debug session stopped for URL: " + remoteUrl); 131 | } 132 | 133 | boolean handled = false; 134 | boolean unusedServer; 135 | 136 | synchronized (ourServerMap) { 137 | unusedServer = myServer.removeServer(this); 138 | if (unusedServer) { 139 | ourServerMap.remove(myServer.getAddress().getPort()); 140 | } 141 | } 142 | 143 | if (unusedServer) { 144 | // shutdown 145 | try { 146 | myServer.stop(1000); 147 | myServer = null; 148 | if (LOG.isDebugEnabled()) { 149 | System.out.println("WebView debug server shutdown."); 150 | LOG.debug("WebView debug server shutdown."); 151 | } 152 | } catch (InterruptedException e) { 153 | e.printStackTrace(); 154 | } finally { 155 | handled = true; 156 | // instance removed and server stopped 157 | onStopped.accept(true); 158 | } 159 | } 160 | 161 | if (!handled) { 162 | // instance removed, server running 163 | onStopped.accept(false); 164 | } 165 | }; 166 | 167 | if (myDebugger instanceof JfxDebuggerProxy) { 168 | // release from break point or it has a tendency to core dump 169 | ((JfxDebuggerProxy) myDebugger).removeAllBreakpoints(() -> { 170 | ((JfxDebuggerProxy) myDebugger).releaseDebugger(true, () -> { 171 | // doing this if was stopped at a break point will core dump the whole app 172 | myDebugger.setEnabled(false); 173 | myDebugger.setMessageCallback(null); 174 | action.run(); 175 | }); 176 | }); 177 | } else { 178 | myDebugger.setEnabled(false); 179 | myDebugger.setMessageCallback(null); 180 | action.run(); 181 | } 182 | }); 183 | } else { 184 | // have not idea since this instance was already disconnected 185 | onStopped.accept(null); 186 | } 187 | } 188 | 189 | @Override 190 | public void onOpen() { 191 | if (myDebugger instanceof JfxDebuggerProxy) { 192 | ((JfxDebuggerProxy) myDebugger).onOpen(); 193 | } 194 | } 195 | 196 | @Override 197 | public void pageReloading() { 198 | if (myDebugger instanceof JfxDebuggerProxy && isDebuggerConnected()) { 199 | ((JfxDebuggerProxy) myDebugger).pageReloading(); 200 | } 201 | } 202 | 203 | @Override 204 | public void reloadPage() { 205 | if (myDebugger instanceof JfxDebuggerProxy) { 206 | ((JfxDebuggerProxy) myDebugger).reloadPage(); 207 | } 208 | } 209 | 210 | @Override 211 | public void onClosed(final int code, final String reason, final boolean remote) { 212 | if (myDebugger instanceof JfxDebuggerProxy) { 213 | ((JfxDebuggerProxy) myDebugger).onClosed(code, reason, remote); 214 | } 215 | } 216 | 217 | @Override 218 | public void log(final String type, final long timestamp, final JSObject args) { 219 | if (myDebugger instanceof JfxDebuggerProxy) { 220 | ((JfxDebuggerProxy) myDebugger).log(type, timestamp, args); 221 | } 222 | } 223 | 224 | @Override 225 | public void setDebugOnLoad(final DebugOnLoad debugOnLoad) { 226 | if (myDebugger instanceof JfxDebuggerProxy) { 227 | ((JfxDebuggerProxy) myDebugger).setDebugOnLoad(debugOnLoad); 228 | } 229 | } 230 | 231 | @Override 232 | public DebugOnLoad getDebugOnLoad() { 233 | if (myDebugger instanceof JfxDebuggerProxy) { 234 | return ((JfxDebuggerProxy) myDebugger).getDebugOnLoad(); 235 | } 236 | return DebugOnLoad.NONE; 237 | } 238 | 239 | @Override 240 | public void debugBreak() { 241 | if (myDebugger instanceof JfxDebuggerProxy) { 242 | ((JfxDebuggerProxy) myDebugger).debugBreak(); 243 | } 244 | } 245 | 246 | @Override 247 | public boolean isDebuggerPaused() { 248 | if (myDebugger instanceof JfxDebuggerProxy) { 249 | return ((JfxDebuggerProxy) myDebugger).isDebuggerPaused(); 250 | } 251 | return false; 252 | } 253 | 254 | @Override 255 | public void releaseDebugger(final boolean shuttingDown, @Nullable Runnable runnable) { 256 | if (myDebugger instanceof JfxDebuggerProxy) { 257 | ((JfxDebuggerProxy) myDebugger).releaseDebugger(shuttingDown, runnable); 258 | } 259 | } 260 | 261 | @Override 262 | public void removeAllBreakpoints(@Nullable Runnable runnable) { 263 | if (myDebugger instanceof JfxDebuggerProxy) { 264 | ((JfxDebuggerProxy) myDebugger).removeAllBreakpoints(runnable); 265 | } 266 | } 267 | 268 | @Override 269 | public void pageLoadComplete() { 270 | if (myDebugger instanceof JfxDebuggerProxy) { 271 | ((JfxDebuggerProxy) myDebugger).pageLoadComplete(); 272 | } 273 | } 274 | 275 | public void sendMessageToBrowser(final String data) { 276 | Platform.runLater(() -> myDebugger.sendMessage(data)); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxConsoleApiArgs.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import com.vladsch.boxed.json.BoxedJsValue; 29 | 30 | public class JfxConsoleApiArgs { 31 | private final Object[] myArgs; 32 | private final String myLogType; 33 | private final BoxedJsValue[] myJsonParams; 34 | private long myTimestamp; 35 | private String myPausedParam; 36 | 37 | public JfxConsoleApiArgs(final Object[] args, final String logType, final long timestamp) { 38 | myArgs = args; 39 | myLogType = logType; 40 | myJsonParams = new BoxedJsValue[args.length]; 41 | myTimestamp = timestamp; 42 | myPausedParam = null; 43 | } 44 | 45 | public String getPausedParam() { 46 | return myPausedParam; 47 | } 48 | 49 | public void setPausedParam(final String pausedParam) { 50 | myPausedParam = pausedParam; 51 | } 52 | 53 | public void setParamJson(int paramIndex, BoxedJsValue paramJson) { 54 | myJsonParams[paramIndex] = paramJson; 55 | } 56 | 57 | public void clearAll() { 58 | int iMax = myArgs.length; 59 | for (int i = 0; i < iMax; i++) { 60 | myArgs[i] = null; 61 | myJsonParams[i] = null; 62 | } 63 | } 64 | 65 | public long getTimestamp() { 66 | return myTimestamp; 67 | } 68 | 69 | public Object[] getArgs() { 70 | return myArgs; 71 | } 72 | 73 | public String getLogType() { 74 | return myLogType; 75 | } 76 | 77 | public BoxedJsValue[] getJsonParams() { 78 | return myJsonParams; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxDebugProxyJsBridge.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import netscape.javascript.JSObject; 29 | 30 | public interface JfxDebugProxyJsBridge { 31 | void consoleLog(String type, JSObject args); 32 | void println(String text); 33 | void print(String text); 34 | void pageLoadComplete(); 35 | void setEventHandledBy(String handledBy); 36 | Object getState(String name); 37 | void setState(String name, Object state); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxDebugProxyJsBridgeDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import netscape.javascript.JSObject; 29 | 30 | public class JfxDebugProxyJsBridgeDelegate implements JfxDebugProxyJsBridge { 31 | final private JfxDebugProxyJsBridge myBridge; 32 | 33 | public JfxDebugProxyJsBridgeDelegate(final JfxDebugProxyJsBridge bridge) { 34 | myBridge = bridge; 35 | } 36 | 37 | @Override public void consoleLog(final String type, final JSObject args) {myBridge.consoleLog(type, args);} 38 | 39 | @Override public void println(final String text) {myBridge.println(text);} 40 | 41 | @Override public void print(final String text) {myBridge.print(text);} 42 | 43 | @Override public void pageLoadComplete() {myBridge.pageLoadComplete();} 44 | 45 | @Override public void setEventHandledBy(final String handledBy) {myBridge.setEventHandledBy(handledBy);} 46 | 47 | @Override public Object getState(final String name) {return myBridge.getState(name);} 48 | 49 | @Override public void setState(final String name, final Object state) {myBridge.setState(name, state);} 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxDebuggerAccess.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | public interface JfxDebuggerAccess { 29 | String setArg(Object arg); 30 | void clearArg(); 31 | Object eval(String script); 32 | void pageReloadStarted(); 33 | String jsBridgeHelperScript(); 34 | void onConnectionOpen(); 35 | void onConnectionClosed(); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxDebuggerConnector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | public interface JfxDebuggerConnector extends JfxDebuggerProxy { 29 | void sendMessageToBrowser(String data); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxDebuggerProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import netscape.javascript.JSObject; 29 | import org.jetbrains.annotations.Nullable; 30 | 31 | public interface JfxDebuggerProxy { 32 | void onOpen(); 33 | void onClosed(int code, String reason, boolean remote); 34 | void log(String type, final long timestamp, final JSObject args); 35 | void debugBreak(); 36 | void pageReloading(); 37 | void reloadPage(); 38 | void setDebugOnLoad(DebugOnLoad debugOnLoad); 39 | DebugOnLoad getDebugOnLoad(); 40 | boolean isDebuggerPaused(); 41 | void releaseDebugger(final boolean shuttingDown, @Nullable Runnable runnable); 42 | void removeAllBreakpoints(@Nullable Runnable runAfter); 43 | void pageLoadComplete(); 44 | 45 | enum DebugOnLoad { 46 | NONE, 47 | ON_BRIDGE_CONNECT, 48 | ON_INJECT_HELPER, 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxScriptArgAccessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | public interface JfxScriptArgAccessor { 29 | Object getArg(); 30 | Object getConsoleArg(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxScriptArgAccessorDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | public class JfxScriptArgAccessorDelegate implements JfxScriptArgAccessor { 29 | final private JfxScriptArgAccessor myAccessor; 30 | 31 | public JfxScriptArgAccessorDelegate(final JfxScriptArgAccessor accessor) { 32 | myAccessor = accessor; 33 | } 34 | 35 | @Override public Object getArg() { 36 | return myAccessor.getArg(); 37 | } 38 | 39 | @Override public Object getConsoleArg() { 40 | return myAccessor.getConsoleArg(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxScriptStateProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import com.vladsch.boxed.json.BoxedJsObject; 29 | import org.jetbrains.annotations.NotNull; 30 | 31 | public interface JfxScriptStateProvider { 32 | @NotNull BoxedJsObject getState(); 33 | void setState(@NotNull BoxedJsObject state); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/JfxWebSocketServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import org.java_websocket.WebSocket; 29 | import org.java_websocket.exceptions.WebsocketNotConnectedException; 30 | import org.java_websocket.framing.CloseFrame; 31 | import org.java_websocket.handshake.ClientHandshake; 32 | import org.java_websocket.server.WebSocketServer; 33 | import org.jetbrains.annotations.NotNull; 34 | import org.jetbrains.annotations.Nullable; 35 | 36 | import java.net.InetSocketAddress; 37 | import java.nio.ByteBuffer; 38 | import java.nio.channels.NotYetConnectedException; 39 | import java.util.HashMap; 40 | import java.util.concurrent.atomic.AtomicInteger; 41 | import java.util.function.Consumer; 42 | 43 | public class JfxWebSocketServer extends WebSocketServer { 44 | public static final String WEB_SOCKET_RESOURCE = "/?%s"; 45 | private static final String CHROME_DEBUG_URL = "chrome-devtools://devtools/bundled/inspector.html?ws=localhost:"; 46 | 47 | final private HashMap myConnections = new HashMap<>(); 48 | final private HashMap myServers = new HashMap<>(); 49 | final private HashMap myServerIds = new HashMap<>(); 50 | private Consumer onFailure; 51 | private Consumer onStart; 52 | private final AtomicInteger myServerUseCount = new AtomicInteger(0); 53 | final LogHandler LOG = LogHandler.getInstance(); 54 | 55 | public JfxWebSocketServer(InetSocketAddress address, @Nullable Consumer onFailure, @Nullable Consumer onStart) { 56 | super(address, 4); 57 | this.onFailure = onFailure; 58 | this.onStart = onStart; 59 | } 60 | 61 | public boolean isDebuggerConnected(JfxDebuggerConnector server) { 62 | String resourceId = myServerIds.get(server); 63 | WebSocket conn = myConnections.get(resourceId); 64 | return conn != null; 65 | } 66 | 67 | public boolean send(JfxDebuggerConnector server, String data) throws NotYetConnectedException { 68 | String resourceId = myServerIds.get(server); 69 | WebSocket conn = myConnections.get(resourceId); 70 | if (conn == null) { 71 | return false; 72 | } 73 | 74 | if (LOG.isDebugEnabled()) System.out.println("sending to " + conn.getRemoteSocketAddress() + ": " + data); 75 | try { 76 | conn.send(data); 77 | } catch (WebsocketNotConnectedException e) { 78 | myConnections.put(resourceId, null); 79 | return false; 80 | } 81 | return true; 82 | } 83 | 84 | @NotNull 85 | public String getDebugUrl(JfxDebuggerConnector server) { 86 | // Chrome won't launch first session if we have a query string 87 | return CHROME_DEBUG_URL + super.getPort() + myServerIds.get(server); 88 | } 89 | 90 | public void addServer(JfxDebuggerConnector debugServer, int instanceID) { 91 | String resourceId = instanceID == 0 ? "/" : String.format(WEB_SOCKET_RESOURCE, instanceID); 92 | if (myServers.containsKey(resourceId)) { 93 | throw new IllegalStateException("Resource id " + resourceId + " is already handled by " + myServers.get(resourceId)); 94 | } 95 | myConnections.put(resourceId, null); 96 | myServers.put(resourceId, debugServer); 97 | myServerIds.put(debugServer, resourceId); 98 | myServerUseCount.incrementAndGet(); 99 | } 100 | 101 | public boolean removeServer(JfxDebuggerConnector server) { 102 | String resourceId = myServerIds.get(server); 103 | if (resourceId != null) { 104 | WebSocket conn = myConnections.get(resourceId); 105 | if (conn != null) { 106 | conn.close(); 107 | } 108 | 109 | myServerIds.remove(server); 110 | myConnections.remove(resourceId); 111 | myServers.remove(resourceId); 112 | int serverUseCount = myServerUseCount.decrementAndGet(); 113 | if (serverUseCount < 0) { 114 | LOG.error("Internal error: server use count <0"); 115 | myServerUseCount.set(0); 116 | } 117 | } 118 | return myServerUseCount.get() <= 0; 119 | } 120 | 121 | @Override 122 | public void onOpen(WebSocket conn, ClientHandshake handshake) { 123 | String resourceId = conn.getResourceDescriptor(); 124 | 125 | if (!myConnections.containsKey(resourceId)) { 126 | System.out.println("new connection to " + conn.getRemoteSocketAddress() + " rejected"); 127 | if (LOG.isDebugEnabled()) System.out.println("new connection to " + conn.getRemoteSocketAddress() + " rejected"); 128 | conn.close(CloseFrame.REFUSE, "No JavaFX WebView Debugger Instance"); 129 | } else { 130 | WebSocket otherConn = myConnections.get(resourceId); 131 | if (otherConn != null) { 132 | // We will disconnect the other 133 | System.out.println("closing old connection to " + conn.getRemoteSocketAddress()); 134 | if (LOG.isDebugEnabled()) System.out.println("closing old connection to " + conn.getRemoteSocketAddress()); 135 | otherConn.close(CloseFrame.GOING_AWAY, "New Dev Tools connected"); 136 | } 137 | 138 | myConnections.put(resourceId, conn); 139 | if (myServers.containsKey(resourceId)) { 140 | myServers.get(resourceId).onOpen(); 141 | } 142 | System.out.println("new connection to " + conn.getRemoteSocketAddress()); 143 | if (LOG.isDebugEnabled()) System.out.println("new connection to " + conn.getRemoteSocketAddress()); 144 | } 145 | } 146 | 147 | @Override 148 | public void onClose(WebSocket conn, int code, String reason, boolean remote) { 149 | String resourceId = conn.getResourceDescriptor(); 150 | 151 | WebSocket otherConn = myConnections.get(resourceId); 152 | if (otherConn == conn) { 153 | myConnections.put(resourceId, null); 154 | if (myServers.containsKey(resourceId)) { 155 | myServers.get(resourceId).onClosed(code, reason, remote); 156 | } 157 | System.out.println("closed " + conn.getRemoteSocketAddress() + " with exit code " + code + " additional info: " + reason); 158 | if (LOG.isDebugEnabled()) System.out.println("closed " + conn.getRemoteSocketAddress() + " with exit code " + code + " additional info: " + reason); 159 | } 160 | } 161 | 162 | @Override 163 | public void onMessage(WebSocket conn, String message) { 164 | String resourceId = conn.getResourceDescriptor(); 165 | 166 | if (myServers.containsKey(resourceId)) { 167 | myServers.get(resourceId).sendMessageToBrowser(message); 168 | if (LOG.isDebugEnabled()) System.out.println("received from " + conn.getRemoteSocketAddress() + ": " + message); 169 | } else { 170 | System.out.println("connection to " + conn.getRemoteSocketAddress() + " closed"); 171 | if (LOG.isDebugEnabled()) System.out.println("connection to " + conn.getRemoteSocketAddress() + " closed"); 172 | conn.close(); 173 | } 174 | } 175 | 176 | @Override 177 | public void onMessage(WebSocket conn, ByteBuffer message) { 178 | if (LOG.isDebugEnabled()) System.out.println("received ByteBuffer from " + conn.getRemoteSocketAddress()); 179 | } 180 | 181 | @Override 182 | public void stop(final int timeout) throws InterruptedException { 183 | try { 184 | super.stop(timeout); 185 | } catch (Exception ex) { 186 | // nothing to do, handleFatal will clean up 187 | } 188 | } 189 | 190 | @Override 191 | public void onError(WebSocket conn, Exception ex) { 192 | if (conn == null) { 193 | if (LOG.isDebugEnabled()) { 194 | System.err.println("an error occurred on connection null :" + ex); 195 | LOG.error("an error occurred on connection null :", ex); 196 | } 197 | } else { 198 | if (LOG.isDebugEnabled()) { 199 | System.err.println("an error occurred on connection " + conn.getRemoteSocketAddress() + ":" + ex); 200 | LOG.error("an error occurred on connection " + conn.getRemoteSocketAddress() + ":", ex); 201 | } 202 | } 203 | 204 | onStart = null; 205 | if (onFailure != null) { 206 | Consumer failure = onFailure; 207 | onFailure = null; 208 | failure.accept(ex); 209 | } 210 | } 211 | 212 | @Override 213 | public void onStart() { 214 | onFailure = null; 215 | if (onStart != null) { 216 | Consumer start = onStart; 217 | onStart = null; 218 | start.accept(this); 219 | } 220 | if (LOG.isDebugEnabled()) { 221 | System.out.println("server started successfully"); 222 | LOG.debug("server started successfully"); 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/main/java/com/vladsch/javafx/webview/debugger/LogHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2020 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | package com.vladsch.javafx.webview.debugger; 27 | 28 | import org.jetbrains.annotations.NotNull; 29 | 30 | public abstract class LogHandler { 31 | // NOTE: this avoids conflicts with loading Logger in IntelliJ, set LogHandler.LOG_HANDLER by application 32 | public static LogHandler LOG_HANDLER = LogHandler.NULL; 33 | 34 | public static LogHandler getInstance() { 35 | return LOG_HANDLER; 36 | } 37 | 38 | public abstract void trace(@NotNull String message); 39 | 40 | public abstract void trace(@NotNull String message, @NotNull Throwable t); 41 | 42 | public abstract void trace(@NotNull Throwable t); 43 | 44 | public abstract boolean isTraceEnabled(); 45 | 46 | public abstract void debug(@NotNull String message); 47 | 48 | public abstract void debug(@NotNull String message, @NotNull Throwable t); 49 | 50 | public abstract void debug(@NotNull Throwable t); 51 | 52 | public abstract void error(@NotNull String message); 53 | 54 | public abstract void error(@NotNull String message, @NotNull Throwable t); 55 | 56 | public abstract void error(@NotNull Throwable t); 57 | 58 | public abstract void info(@NotNull String message); 59 | 60 | public abstract void info(@NotNull String message, @NotNull Throwable t); 61 | 62 | public abstract void info(@NotNull Throwable t); 63 | 64 | public abstract boolean isDebugEnabled(); 65 | 66 | public abstract void warn(@NotNull String message); 67 | 68 | public abstract void warn(@NotNull String message, @NotNull Throwable t); 69 | 70 | public abstract void warn(@NotNull Throwable t); 71 | 72 | final public static LogHandler NULL = new LogHandler() { 73 | public @Override 74 | void trace(@NotNull String message) {} 75 | 76 | public @Override 77 | void trace(@NotNull String message, @NotNull Throwable t) {} 78 | 79 | public @Override 80 | void trace(@NotNull Throwable t) {} 81 | 82 | public @Override 83 | boolean isTraceEnabled() {return false;} 84 | 85 | public @Override 86 | void debug(@NotNull String message) {} 87 | 88 | public @Override 89 | void debug(@NotNull String message, @NotNull Throwable t) {} 90 | 91 | public @Override 92 | void debug(@NotNull Throwable t) {} 93 | 94 | public @Override 95 | void error(@NotNull String message) {} 96 | 97 | public @Override 98 | void error(@NotNull String message, @NotNull Throwable t) {} 99 | 100 | public @Override 101 | void error(@NotNull Throwable t) {} 102 | 103 | public @Override 104 | void info(@NotNull String message) {} 105 | 106 | public @Override 107 | void info(@NotNull String message, @NotNull Throwable t) {} 108 | 109 | public @Override 110 | void info(@NotNull Throwable t) {} 111 | 112 | public @Override 113 | boolean isDebugEnabled() {return false;} 114 | 115 | public @Override 116 | void warn(@NotNull String message) {} 117 | 118 | public @Override 119 | void warn(@NotNull String message, @NotNull Throwable t) {} 120 | 121 | public @Override 122 | void warn(@NotNull Throwable t) {} 123 | }; 124 | } 125 | -------------------------------------------------------------------------------- /src/main/javadoc/overview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

JavaFX WebView Debugger

7 |

Library implementing WebSocket server for JavaFX WebView debugger to Chrome Dev Tools.

8 |

Creates a comfortable debugging environment for script debugging in JavaFX WebView:

9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/javadoc/overview.md: -------------------------------------------------------------------------------- 1 | **JavaFX WebView Debugger** 2 | 3 | Library implementing WebSocket server for JavaFX WebView debugger to Chrome Dev Tools. 4 | 5 | Creates a comfortable debugging environment for script debugging in JavaFX WebView: 6 | 7 | * all `console` functions: `assert`, `clear`, `count`, `debug`, `dir`, `dirxml`, `error`, 8 | `exception`, `group`, `groupCollapsed`, `groupEnd`, `info`, `log`, `profile`, `profileEnd`, 9 | `select`, `table`, `time`, `timeEnd`, `trace`, `warn`. With added extras to output to the Java 10 | console: `print`, `println` 11 | * all commandLineAPI functions implemented by JavaFX WebView: `$`, `$$`, `$x`, `dir`, `dirxml`, 12 | `keys`, `values`, `profile`, `profileEnd`, `table`, `monitorEvents`, `unmonitorEvents`, 13 | `inspect`, `copy`, `clear`, `getEventListeners`, `$0`, `$_`, `$exception` 14 | -------------------------------------------------------------------------------- /src/main/resources/console-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | *

4 | * Copyright (c) 2018-2018 Vladimir Schneider (https://github.com/vsch) 5 | *

6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | *

13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | *

16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE 23 | * 24 | */ 25 | 26 | function testConsoleLog() { 27 | "use strict"; 28 | 29 | let onLoadScroll = {x: window.pageXOffset, y: window.pageYOffset}; 30 | 31 | console.group("Console Tests"); 32 | console.group("Log Types"); 33 | console.log("Log text", onLoadScroll, null); 34 | console.warn("Warning text", onLoadScroll, null); 35 | console.error("Error text", onLoadScroll, null); 36 | console.debug("Debug text", onLoadScroll, null); 37 | console.groupEnd(); 38 | 39 | console.table(onLoadScroll); 40 | console.assert(false, "Assertion failed"); 41 | console.groupEnd(); 42 | } 43 | 44 | function testDebugBreak() { 45 | debugBreak(); 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/main/resources/markdown-navigator.js: -------------------------------------------------------------------------------- 1 | //# sourceURL=__MarkdownNavigatorHelperModuleSource__ 2 | /* 3 | * The MIT License (MIT) 4 | *

5 | * Copyright (c) 2018-2018 Vladimir Schneider (https://github.com/vsch) 6 | *

7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | *

14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | *

17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE 24 | * 25 | */ 26 | 27 | // noinspection ES6ConvertVarToLetConst 28 | var markdownNavigator; 29 | 30 | // do nothing until jsBridge is established 31 | // noinspection ES6ConvertVarToLetConst 32 | var debugBreak = () => { 33 | }; 34 | 35 | // lets keep this around for our scripts 36 | // noinspection ES6ConvertVarToLetConst 37 | var __api = console.__commandLineAPI || {}; 38 | 39 | // map console functions to our bridge 40 | // noinspection ES6ConvertVarToLetConst 41 | var console = (() => { 42 | let __tmp = { 43 | logDebugBreak: () => { 44 | }, 45 | consoleLog: (type, args) => { 46 | markdownNavigator.consoleLog(type, args); 47 | }, 48 | println: text => markdownNavigator.println(text), 49 | print: text => markdownNavigator.print(text), 50 | }; 51 | 52 | return { 53 | assert: function () { return __tmp.consoleLog("assert", arguments); }, 54 | clear: function () { return __tmp.consoleLog("clear", arguments); }, 55 | count: function () { return __tmp.consoleLog("count", arguments); }, 56 | debug: function () { return __tmp.consoleLog("debug", arguments); }, 57 | dir: function () { return __tmp.consoleLog("dir", arguments); }, 58 | dirxml: function () { return __tmp.consoleLog("dirxml", arguments); }, 59 | error: function () { return __tmp.consoleLog("error", arguments); }, 60 | exception: function () { return __tmp.consoleLog("exception", arguments); }, 61 | group: function () { return __tmp.consoleLog("startGroup", arguments); }, 62 | groupCollapsed: function () { return __tmp.consoleLog("startGroupCollapsed", arguments); }, 63 | groupEnd: function () { return __tmp.consoleLog("endGroup", arguments); }, 64 | info: function () { return __tmp.consoleLog("info", arguments); }, 65 | log: function () { return __tmp.consoleLog("log", arguments); }, 66 | profile: function () { return __tmp.consoleLog("profile", arguments); }, 67 | profileEnd: function () { return __tmp.consoleLog("profileEnd", arguments); }, 68 | select: function () { return __tmp.consoleLog("select", arguments); }, 69 | table: function () { return __tmp.consoleLog("table", arguments); }, 70 | time: function () { return __tmp.consoleLog("time", arguments); }, 71 | timeEnd: function () { return __tmp.consoleLog("timeEnd", arguments); }, 72 | trace: function () { return __tmp.consoleLog("trace", arguments); }, 73 | warn: function () { return __tmp.consoleLog("warning", arguments); }, 74 | print: text => __tmp.print(text), 75 | println: text => __tmp.println(text), 76 | setJsBridge: (jsBridge) => { 77 | __tmp = jsBridge; 78 | // function for cached logs 79 | return ((type, args) => { 80 | __tmp.consoleLog(type, args); 81 | }); 82 | }, 83 | }; 84 | })(); 85 | 86 | // noinspection JSValidateTypes 87 | window.console = console; 88 | 89 | markdownNavigator = (function () { 90 | "use strict"; 91 | 92 | // FIX: change this for real implementation using computed CSS properties of element 93 | // and an overlay element 94 | const HIGHLIGHT = "markdown-navigator-highlight"; 95 | const HIGHLIGHT_STYLE = document.createElement("style"); 96 | 97 | // just so we get a color chooser in IDEA, uncomment 98 | HIGHLIGHT_STYLE.textContent = `.${HIGHLIGHT} { 99 | background-color: rgba(255, 0, 255, 0.07) !important; 100 | }`; 101 | 102 | let __markdownNavigator, 103 | __tmp = { 104 | __state: {}, 105 | __onJsBridge: [], 106 | __onJsConsole: [], 107 | __consoleSetJsBridge: console.setJsBridge, // functions to use before JSBridge is established 108 | onJsBridge: () => { 109 | }, 110 | onJsConsole: () => { 111 | }, 112 | }, 113 | __consoleLog = (type, args) => { 114 | }, 115 | __lastHighlight = null; 116 | 117 | delete console["setJsBridge"]; 118 | 119 | __tmp.onJsBridge = op => { 120 | __tmp.__onJsBridge[__tmp.__onJsBridge.length] = op; 121 | }; 122 | 123 | __tmp.onJsConsole = op => { 124 | __tmp.__onJsConsole[__tmp.__onJsConsole.length] = op; 125 | }; 126 | 127 | let __unbridged = { 128 | // functions to be replaced with real ones when JsBridge is established 129 | setEventHandledBy: handledBy => { 130 | }, 131 | 132 | getState: name => { 133 | return __tmp.__state[name] || null; 134 | }, 135 | 136 | setState: (name, state) => { 137 | __tmp.__state[name] = state; 138 | }, 139 | 140 | toggleTask: function (pos) { 141 | __tmp.onJsBridge(() => { 142 | __markdownNavigator.toggleTask(pos); 143 | }); 144 | }, 145 | 146 | onJsBridge: function (op) { 147 | __tmp.onJsBridge(op); 148 | }, 149 | 150 | // functions mimicking jsBridge until it is connected 151 | consoleLog: (type, args) => { 152 | __tmp.onJsConsole(() => { 153 | __consoleLog(type, args); 154 | }); 155 | }, 156 | 157 | println: text => { 158 | __tmp.onJsConsole(() => { 159 | console.println(text); 160 | }); 161 | }, 162 | 163 | print: text => { 164 | __tmp.onJsConsole(() => { 165 | console.print(text); 166 | }); 167 | }, 168 | 169 | highlightNode: (nodeOrdinals) => { 170 | function getChildNode(node, nodeOrdinal) { 171 | let iMax = node.childNodes.length; 172 | let index = 0; 173 | for (let i = 0; i < iMax; i++) { 174 | let child = node.childNodes.item(i); 175 | if (child.nodeName.startsWith("#") && (child.nodeName !== "#text" || child.textContent.trim() === "")) { 176 | // skip non-element nodes or empty text 177 | continue; 178 | } 179 | 180 | if (index === nodeOrdinal) { 181 | return child; 182 | } 183 | index++; 184 | } 185 | return null; 186 | } 187 | 188 | // need to find the node, path is ordinal in parent, in reverse order, all the way to document, first index to be ignored 189 | let iMax = nodeOrdinals.length; 190 | let node = document; 191 | try { 192 | for (let i = iMax; i-- > 1;) { 193 | let nodeOrdinal = nodeOrdinals[i]; 194 | let child = getChildNode(node, nodeOrdinal); 195 | 196 | if (child === null) { 197 | if (i === 1) { 198 | // if last one take the parent 199 | break; 200 | } 201 | 202 | console.error("Node ordinal not in children", nodeOrdinal, node, nodeOrdinals); 203 | node = null; 204 | break; 205 | } 206 | 207 | if (child.nodeName.startsWith("#")) { 208 | // FIX: when overlays are implemented to handle non-element nodes, highlight the child 209 | break; 210 | } 211 | 212 | node = child; 213 | } 214 | // console.debug("Final node", node); 215 | if (node !== null && node !== __lastHighlight) { 216 | __unbridged.hideHighlight(); 217 | node.classList.add(HIGHLIGHT); 218 | __lastHighlight = node; 219 | } 220 | } catch (e) { 221 | console.error(e) 222 | } 223 | }, 224 | 225 | hideHighlight: () => { 226 | if (__lastHighlight) { 227 | __lastHighlight.classList.remove(HIGHLIGHT); 228 | if (__lastHighlight.classList.length === 0) { 229 | __lastHighlight.removeAttribute("class"); 230 | } 231 | __lastHighlight = null; 232 | } 233 | }, 234 | 235 | setJsBridge: jsBridge => { 236 | // map to real JsBridge 237 | __consoleLog = __tmp.__consoleSetJsBridge(jsBridge); 238 | __markdownNavigator = jsBridge; 239 | debugBreak = jsBridge.debugBreak; 240 | 241 | // we don't need these anymore 242 | delete __unbridged["consoleLog"]; 243 | delete __unbridged["setJsBridge"]; 244 | delete __unbridged["println"]; 245 | delete __unbridged["print"]; 246 | 247 | // these now point directly to the jsBridge implementations or invocations 248 | __unbridged.setEventHandledBy = (name) => jsBridge.setEventHandledBy(name); 249 | __unbridged.getState = (name) => jsBridge.getState(name); 250 | __unbridged.setState = (name, state) => jsBridge.setState(name, state); 251 | __unbridged.toggleTask = position => jsBridge.toggleTask(position); 252 | __unbridged.onJsBridge = op => op(); 253 | 254 | document.querySelector("head").appendChild(HIGHLIGHT_STYLE); 255 | console.debug(`Created ${HIGHLIGHT} style element`, HIGHLIGHT_STYLE); 256 | 257 | // dump any accumulated console/print before bridge connected 258 | for (const __onJsConsoleItem of __tmp.__onJsConsole) { 259 | try { 260 | __onJsConsoleItem(); 261 | } catch (e) { 262 | console.println("onJsConsole exception: calling " + __onJsConsoleItem); 263 | console.println("onJsConsole exception: " + e); 264 | } 265 | } 266 | 267 | // save any state changes requested before jsBridge was setup 268 | console.groupCollapsed("cachedState"); 269 | for (let f in __tmp.__state) { 270 | if (__tmp.__state.hasOwnProperty(f)) { 271 | console.log(f, __tmp.__state[f]); 272 | // console.println(name + " = " + JSON.stringify( __state[name],null,2)); 273 | __markdownNavigator.setState(f, __tmp.__state[f]); 274 | } 275 | } 276 | console.groupEnd(); 277 | 278 | // run any ops needed on connection 279 | for (const __onJsBridgeItem of __tmp.__onJsBridge) { 280 | console.debug("onLoad", __onJsBridgeItem); 281 | try { 282 | __onJsBridgeItem(); 283 | } catch (e) { 284 | console.println("onJsBridge exception: calling " + __onJsBridgeItem); 285 | console.println("onJsBridge exception: " + e); 286 | } 287 | } 288 | console.groupEnd(); 289 | 290 | __tmp = null; 291 | 292 | // signal JsBridge connection complete 293 | __markdownNavigator.pageLoadComplete(); 294 | }, 295 | }; 296 | 297 | return __unbridged; 298 | })(); 299 | 300 | --------------------------------------------------------------------------------