mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 01:48:18 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f2f7d2fd4 | ||
|
|
99544b527d | ||
|
|
614fcbd2ba | ||
|
|
b04e4328a6 | ||
|
|
1a00cdb291 | ||
|
|
d1fd889433 | ||
|
|
a7e4cac6df | ||
|
|
c6df24ab95 | ||
|
|
23184ea4c2 | ||
|
|
b7ba47b811 | ||
|
|
033f5c80ab | ||
|
|
0cbb56ca9b | ||
|
|
684dd39181 | ||
|
|
52ab704acd | ||
|
|
45e2dc2fe1 | ||
|
|
30ed5dea3b | ||
|
|
0eb44130a6 | ||
|
|
d1e7c86092 | ||
|
|
690f9dc23a | ||
|
|
27ecd41b68 | ||
|
|
756a1cea29 | ||
|
|
fc2ce97a23 | ||
|
|
de8abc62bc | ||
|
|
8cc31476b3 | ||
|
|
494bb83cbf | ||
|
|
9fca5cbe8c | ||
|
|
5f0a02b17f | ||
|
|
da36e8e457 | ||
|
|
3dbfd8ef2a | ||
|
|
4a31b83eb9 | ||
|
|
261c06f1d6 | ||
|
|
130d79f6c2 | ||
|
|
8baf894fc9 | ||
|
|
92bd003122 | ||
|
|
13f116b3b3 | ||
|
|
140e48cbd1 | ||
|
|
7e8f471df4 | ||
|
|
09905eb85a | ||
|
|
e76773e8e4 | ||
|
|
c98c962172 | ||
|
|
b3501d529a |
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,5 +1,43 @@
|
||||
# Change Log
|
||||
|
||||
### version 0.7.1
|
||||
* Added the [Buy Me A Coffee](https://www.buymeacoffee.com/adelphes) link to the README
|
||||
|
||||
### version 0.7.0
|
||||
* Fix logcat not displaying
|
||||
* Fix breakpoints not triggering on Windows
|
||||
* Added kotlin folder to list of known source locations
|
||||
* Upgraded dependencies to resolve a number of security vulnerabilites
|
||||
* Updated README with info about prelaunch build task
|
||||
* Added MIT license file
|
||||
|
||||
### version 0.6.2
|
||||
* Fix broken logcat command due to missing dependency
|
||||
|
||||
### version 0.6.1
|
||||
* Regenerate package-lock.json to remove event-stream vulnerability - https://github.com/dominictarr/event-stream/issues/116
|
||||
|
||||
### version 0.6.0
|
||||
* Fix issue with breakpoints not enabling correctly
|
||||
* Fix issue with JDWP failure on breakpoint hit
|
||||
* Added support for diagnostic logs using trace configuration option
|
||||
* Updated default apkFile path to match current releases of Android Studio
|
||||
* Updated package dependencies
|
||||
|
||||
### version 0.5.0
|
||||
* Debugger support for Kotlin source files
|
||||
* Exception UI
|
||||
* Fixed some console display issues
|
||||
|
||||
### version 0.4.1
|
||||
* One day I will learn to update the changelog **before** I hit publish
|
||||
* Updated changelog
|
||||
|
||||
### version 0.4.0
|
||||
* Debugger performance improvements
|
||||
* Fixed exception details not being displayed in locals
|
||||
* Fixed some logcat display issues
|
||||
|
||||
### version 0.3.1
|
||||
* Bug fixes
|
||||
* Fix issue with exception breaks crashing debugger
|
||||
@@ -29,4 +67,4 @@ Initial release
|
||||
* Simple watch expressions
|
||||
* Breakpoints
|
||||
* Large array chunking (performance)
|
||||
* Stale build detection
|
||||
* Stale build detection
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Dave Holoway
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
48
README.md
48
README.md
@@ -20,7 +20,7 @@ You must have [Android SDK Platform Tools](https://developer.android.com/studio/
|
||||
* This is a preview version so expect the unexpected. Please log any issues you find on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
|
||||
* This extension **will not build your app**.
|
||||
If you use gradle (or Android Studio), you can build your app from the command-line using `./gradlew assembleDebug`.
|
||||
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way.
|
||||
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way. See the section below on how to configure a VSCode task to automatically build your app before launching a debug session.
|
||||
* Some debugger options are yet to be implemented. You cannot set conditional breakpoints and watch expressions must be simple variables.
|
||||
* If you require a must-have feature that isn't there yet, let us know on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
|
||||
* This extension does not provide any additional code completion or other editing enhancements.
|
||||
@@ -54,6 +54,52 @@ The following settings are used to configure the debugger:
|
||||
]
|
||||
}
|
||||
|
||||
## Building your app automatically
|
||||
|
||||
This extension will not build your App. If you would like to run a build each time a debug session is started, you can add a `preLaunchTask` option to your `launch.json` configuration which invokes a build task.
|
||||
|
||||
#### .vscode/launch.json
|
||||
Add a `preLaunchTask` item to the launch configuration:
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "android",
|
||||
"request": "launch",
|
||||
"name": "App Build & Launch",
|
||||
"preLaunchTask": "run gradle",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Add a new task to run the build command:
|
||||
#### .vscode/tasks.json
|
||||
```json
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "run gradle",
|
||||
"type": "shell",
|
||||
"command": "${workspaceFolder}/gradlew",
|
||||
"args": ["assembleDebug"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Powered by coffee
|
||||
|
||||
The Android Developer Extension is a completely free, fully open-source project. If you've found the extension useful, you
|
||||
can support it by [buying me a coffee](https://www.buymeacoffee.com/adelphes).
|
||||
|
||||
If you use ApplePay or Google Pay, you can scan the code with your phone camera:
|
||||
|
||||

|
||||
|
||||
Every coffee makes a difference, so thanks for adding your support.
|
||||
|
||||
## Questions / Problems
|
||||
|
||||
If you run into any problems, tell us on [GitHub](https://github.com/adelphes/android-dev-ext/issues) or contact me on [Twitter](https://twitter.com/daveholoway).
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const vscode = require('vscode');
|
||||
const { AndroidContentProvider } = require('./src/contentprovider');
|
||||
const { openLogcatWindow } = require('./src/logcat');
|
||||
const state = require('./src/state');
|
||||
|
||||
function getADBPort() {
|
||||
var defaultPort = 5037;
|
||||
@@ -38,6 +39,7 @@ function activate(context) {
|
||||
var spliceparams = [context.subscriptions.length,0].concat(disposables);
|
||||
Array.prototype.splice.apply(context.subscriptions,spliceparams);
|
||||
}
|
||||
|
||||
exports.activate = activate;
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
|
||||
BIN
images/bmac-code.png
Normal file
BIN
images/bmac-code.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
2695
package-lock.json
generated
Normal file
2695
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "android-dev-ext",
|
||||
"displayName": "Android",
|
||||
"description": "Android debugging support for VS Code",
|
||||
"version": "0.4.0",
|
||||
"version": "0.7.1",
|
||||
"publisher": "adelphes",
|
||||
"preview": true,
|
||||
"license": "MIT",
|
||||
@@ -35,12 +35,15 @@
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "java"
|
||||
},
|
||||
{
|
||||
"language": "kotlin"
|
||||
}
|
||||
],
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "android",
|
||||
"label": "Android Debug",
|
||||
"label": "Android",
|
||||
"program": "./src/debugMain.js",
|
||||
"runtime": "node",
|
||||
"configurationAttributes": {
|
||||
@@ -59,7 +62,7 @@
|
||||
"apkFile": {
|
||||
"type": "string",
|
||||
"description": "Fully qualified path to the built APK (Android Application Package)",
|
||||
"default": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk"
|
||||
"default": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk"
|
||||
},
|
||||
"adbPort": {
|
||||
"type": "integer",
|
||||
@@ -90,6 +93,11 @@
|
||||
"type": "string",
|
||||
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
|
||||
"default": ""
|
||||
},
|
||||
"trace": {
|
||||
"type": "boolean",
|
||||
"description": "Set to true to output debugging logs for diagnostics",
|
||||
"default": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,10 +105,10 @@
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "android",
|
||||
"name": "Android Debug",
|
||||
"name": "Android",
|
||||
"request": "launch",
|
||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
|
||||
"apkFile": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk",
|
||||
"adbPort": 5037
|
||||
}
|
||||
],
|
||||
@@ -113,7 +121,7 @@
|
||||
"request": "launch",
|
||||
"name": "${2:Launch App}",
|
||||
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
||||
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/app-debug.apk\"",
|
||||
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk\"",
|
||||
"adbPort": 5037
|
||||
}
|
||||
}
|
||||
@@ -123,23 +131,24 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
||||
"prepare": "node ./node_modules/vscode/bin/install",
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-debugprotocol": "^1.15.0",
|
||||
"vscode-debugadapter": "^1.15.0",
|
||||
"long": "^3.2.0",
|
||||
"ws": "^1.1.1",
|
||||
"long": "^4.0.0",
|
||||
"uuid": "^3.3.2",
|
||||
"vscode-debugadapter": "^1.32.0",
|
||||
"vscode-debugprotocol": "^1.32.0",
|
||||
"ws": "^7.1.2",
|
||||
"xmldom": "^0.1.27",
|
||||
"xpath": "^0.0.23"
|
||||
"xpath": "^0.0.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^2.0.3",
|
||||
"vscode": "^1.0.0",
|
||||
"mocha": "^2.3.3",
|
||||
"eslint": "^3.6.0",
|
||||
"@types/node": "^6.0.40",
|
||||
"@types/mocha": "^2.2.32"
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/node": "^10.12.5",
|
||||
"eslint": "^5.9.0",
|
||||
"mocha": "^5.2.0",
|
||||
"typescript": "^3.1.6",
|
||||
"vscode": "^1.1.26"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
|
||||
provideLogcatDocumentContent(uri) {
|
||||
// LogcatContent depends upon AndroidContentProvider, so we must delay-load this
|
||||
const { LogcatContent } = require('./logcat');
|
||||
var doc = this._docs[uri] = new LogcatContent(this, uri);
|
||||
var doc = this._docs[uri] = new LogcatContent(uri.query);
|
||||
return doc.content;
|
||||
}
|
||||
}
|
||||
|
||||
664
src/debugMain.js
664
src/debugMain.js
@@ -1,14 +1,14 @@
|
||||
'use strict'
|
||||
const {
|
||||
DebugSession,
|
||||
ContinuedEvent, InitializedEvent, ExitedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, ThreadEvent, OutputEvent, Event,
|
||||
Thread, StackFrame, Scope, Source, Handles, Breakpoint } = require('vscode-debugadapter');
|
||||
InitializedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, ThreadEvent, OutputEvent,
|
||||
Thread, StackFrame, Scope, Source, Breakpoint } = require('vscode-debugadapter');
|
||||
|
||||
// node and external modules
|
||||
const crypto = require('crypto');
|
||||
const dom = require('xmldom').DOMParser;
|
||||
const fs = require('fs');
|
||||
const Long = require('long');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const xpath = require('xpath');
|
||||
|
||||
@@ -16,12 +16,12 @@ const xpath = require('xpath');
|
||||
const { ADBClient } = require('./adbclient');
|
||||
const { Debugger } = require('./debugger');
|
||||
const $ = require('./jq-promise');
|
||||
const NumberBaseConverter = require('./nbc');
|
||||
const { AndroidThread } = require('./threads');
|
||||
const { D, isEmptyObject } = require('./util');
|
||||
const { D, onMessagePrint, isEmptyObject } = require('./util');
|
||||
const { AndroidVariables } = require('./variables');
|
||||
const { evaluate } = require('./expressions');
|
||||
const ws_proxy = require('./wsproxy').proxy.Server(6037, 5037);
|
||||
const { JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString } = require('./globals');
|
||||
const { exmsg_var_name, signatureToFullyQualifiedType, ensure_path_end_slash,is_subpath_of,variableRefToThreadId } = require('./globals');
|
||||
|
||||
class AndroidDebugSession extends DebugSession {
|
||||
|
||||
@@ -73,6 +73,9 @@ class AndroidDebugSession extends DebugSession {
|
||||
// flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests
|
||||
this._isDisconnecting = false;
|
||||
|
||||
// trace flag for printing diagnostic messages to the client Output Window
|
||||
this.trace = false;
|
||||
|
||||
// this debugger uses one-based lines and columns
|
||||
this.setDebuggerLinesStartAt1(true);
|
||||
this.setDebuggerColumnsStartAt1(true);
|
||||
@@ -82,7 +85,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
* The 'initialize' request is the first request called by the frontend
|
||||
* to interrogate the features the debug adapter provides.
|
||||
*/
|
||||
initializeRequest(response/*: DebugProtocol.InitializeResponse*/, args/*: DebugProtocol.InitializeRequestArguments*/) {
|
||||
initializeRequest(response/*: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments*/) {
|
||||
|
||||
// This debug adapter implements the configurationDoneRequest.
|
||||
response.body.supportsConfigurationDoneRequest = true;
|
||||
@@ -99,22 +102,39 @@ class AndroidDebugSession extends DebugSession {
|
||||
// we support hit-count conditional breakpoints
|
||||
response.body.supportsHitConditionalBreakpoints = true;
|
||||
|
||||
// we support the new ExceptionInfoRequest
|
||||
response.body.supportsExceptionInfoRequest = true;
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
LOG(msg) {
|
||||
D(msg);
|
||||
this.sendEvent(new OutputEvent(msg));
|
||||
if (!this.trace) {
|
||||
D(msg);
|
||||
}
|
||||
// VSCode no longer auto-newlines output
|
||||
this.sendEvent(new OutputEvent(msg + os.EOL));
|
||||
}
|
||||
|
||||
WARN(msg) {
|
||||
D(msg = 'Warning: '+msg);
|
||||
this.sendEvent(new OutputEvent(msg));
|
||||
// the message will already be sent if trace is enabled
|
||||
if (!this.trace) {
|
||||
this.sendEvent(new OutputEvent(msg + os.EOL));
|
||||
}
|
||||
}
|
||||
|
||||
failRequest(msg, response) {
|
||||
// yeah, it can happen sometimes...
|
||||
this.WARN(msg);
|
||||
msg && this.WARN(msg);
|
||||
if (response) {
|
||||
response.success = false;
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
cancelRequest(msg, response) {
|
||||
D(msg); // just log it in debug - don't output it to the client
|
||||
if (response) {
|
||||
response.success = false;
|
||||
this.sendResponse(response);
|
||||
@@ -129,6 +149,11 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response);
|
||||
}
|
||||
|
||||
cancelRequestThreadNotSuspended(requestName, threadId, response) {
|
||||
// now that vscode can resume threads before the locals,callstack,etc are retrieved, we only need to cancel the request
|
||||
this.cancelRequest(`${requestName} cancelled. Thread ${threadId} is not suspended`, response);
|
||||
}
|
||||
|
||||
getThread(id) {
|
||||
var t;
|
||||
switch(typeof id) {
|
||||
@@ -206,6 +231,10 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
|
||||
launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
|
||||
if (args && args.trace) {
|
||||
this.trace = args.trace;
|
||||
onMessagePrint(this.LOG.bind(this));
|
||||
}
|
||||
|
||||
try { D('Launching: ' + JSON.stringify(args)); } catch(ex) {}
|
||||
// app_src_root must end in a path-separator for correct validation of sub-paths
|
||||
@@ -343,6 +372,9 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.sendResponse(response);
|
||||
return this.dbgr.resume();
|
||||
})
|
||||
.then(() => {
|
||||
this.LOG('Application started');
|
||||
})
|
||||
.fail(e => {
|
||||
// exceptions use message, adbclient uses msg
|
||||
this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available'));
|
||||
@@ -451,18 +483,18 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
catch (err) { continue }
|
||||
// ignore folders not starting with a known top-level Android folder
|
||||
if (!/^(assets|res|src|main|java)([\\/]|$)/.test(p)) continue;
|
||||
if (!/^(assets|res|src|main|java|kotlin)([\\/]|$)/.test(p)) continue;
|
||||
// is this a package folder
|
||||
var pkgmatch = p.match(/^(src|main|java)[\\/](.+)/);
|
||||
var pkgmatch = p.match(/^(src|main|java|kotlin)[\\/](.+)/);
|
||||
if (pkgmatch && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(pkgmatch[2].split(/[\\/]/).pop())) {
|
||||
// looks good - add it to the list
|
||||
const src_folder = pkgmatch[1]; // src, main or java
|
||||
const src_folder = pkgmatch[1]; // src, main, java or kotlin
|
||||
const pkgname = pkgmatch[2].replace(/[\\/]/g,'.');
|
||||
src_packages.packages[pkgname] = {
|
||||
package: pkgname,
|
||||
package_path: fpn,
|
||||
srcroot: path.join(app_root,src_folder),
|
||||
public_classes: subfiles.filter(sf => /^[a-zA-Z_$][a-zA-Z0-9_$]*\.java$/.test(sf)).map(sf => sf.match(/^(.*)\.java$/)[1])
|
||||
public_classes: subfiles.filter(sf => /^[a-zA-Z_$][a-zA-Z0-9_$]*\.(?:java|kt)$/.test(sf)).map(sf => sf.match(/^(.*)\.(?:java|kt)$/)[1])
|
||||
}
|
||||
}
|
||||
// add the subfiles to the list to process
|
||||
@@ -503,7 +535,8 @@ class AndroidDebugSession extends DebugSession {
|
||||
})
|
||||
}
|
||||
|
||||
configurationDoneRequest(response, args) {
|
||||
configurationDoneRequest(response/*, args*/) {
|
||||
D('configurationDoneRequest');
|
||||
this.waitForConfigurationDone.resolve();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
@@ -518,7 +551,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
}
|
||||
|
||||
disconnectRequest(response, args) {
|
||||
disconnectRequest(response/*, args*/) {
|
||||
D('disconnectRequest');
|
||||
this._isDisconnecting = true;
|
||||
// if we're connected, ask ADB to terminate the app
|
||||
@@ -534,13 +567,14 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
|
||||
onBreakpointStateChange(e) {
|
||||
D('onBreakpointStateChange');
|
||||
e.breakpoints.forEach(javabp => {
|
||||
// if there's no associated vsbp we're deleting it, so just ignore the update
|
||||
if (!javabp.vsbp) return;
|
||||
var verified = !!javabp.state.match(/set|enabled/);
|
||||
javabp.vsbp.verified = verified;
|
||||
javabp.vsbp.message = null;
|
||||
this.sendEvent(new BreakpointEvent('updated', javabp.vsbp));
|
||||
this.sendEvent(new BreakpointEvent('changed', javabp.vsbp));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -565,6 +599,14 @@ class AndroidDebugSession extends DebugSession {
|
||||
return bp;
|
||||
}
|
||||
|
||||
const sendBPResponse = (response, breakpoints) => {
|
||||
D('setBreakPointsRequest response ' + JSON.stringify(breakpoints.map(bp => bp.verified)));
|
||||
response.body = {
|
||||
breakpoints,
|
||||
};
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
// the file must lie inside one of the source packages we found (and it must be have a .java extension)
|
||||
var srcfolder = path.dirname(srcfpn);
|
||||
var pkginfo;
|
||||
@@ -572,18 +614,23 @@ class AndroidDebugSession extends DebugSession {
|
||||
if ((pkginfo = this.src_packages.packages[pkg]).package_path === srcfolder) break;
|
||||
pkginfo = null;
|
||||
}
|
||||
// if we didn't find an exact path match, look for a case-insensitive match
|
||||
if (!pkginfo) {
|
||||
for (var pkg in this.src_packages.packages) {
|
||||
if ((pkginfo = this.src_packages.packages[pkg]).package_path.localeCompare(srcfolder, undefined, { sensitivity: 'base' }) === 0) break;
|
||||
pkginfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
// if it's not in our source packages, check if it's in the Android source file cache
|
||||
if (!pkginfo && is_subpath_of(srcfpn, this._android_sources_path)) {
|
||||
// create a fake pkginfo to use to construct the bp
|
||||
pkginfo = { srcroot:this._android_sources_path }
|
||||
}
|
||||
if (!pkginfo || !/\.java$/i.test(srcfpn)) {
|
||||
if (!pkginfo || !/\.(java|kt)$/i.test(srcfpn)) {
|
||||
// source file is not a java file or is outside of the known source packages
|
||||
// just send back a list of unverified breakpoints
|
||||
response.body = {
|
||||
breakpoints: args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid'))
|
||||
};
|
||||
this.sendResponse(response);
|
||||
sendBPResponse(response, args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid')));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -633,7 +680,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
javabp.vsbp.order = idx;
|
||||
javabp_arr.push(javabp);
|
||||
}).
|
||||
then(javabp => _setup_breakpoints(o, ++idx, javabp_arr));
|
||||
then((/*javabp*/) => _setup_breakpoints(o, ++idx, javabp_arr));
|
||||
};
|
||||
|
||||
if (!this._set_breakpoints_queue) {
|
||||
@@ -650,10 +697,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
this._setup_breakpoints(this._queue[0]).then(javabp_arr => {
|
||||
// send back the VS Breakpoint instances
|
||||
var response = this._queue[0].response;
|
||||
response.body = {
|
||||
breakpoints: javabp_arr.map(javabp => javabp.vsbp)
|
||||
};
|
||||
this._dbgr.sendResponse(response);
|
||||
sendBPResponse(response, javabp_arr.map(javabp => javabp.vsbp));
|
||||
// .. and do the next one
|
||||
this._queue.shift();
|
||||
this._next();
|
||||
@@ -681,7 +725,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
|
||||
threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
|
||||
if (this._threads.array.length) {
|
||||
console.log('threadsRequest: ' + this._threads.array.length);
|
||||
D('threadsRequest: ' + this._threads.array.length);
|
||||
response.body = {
|
||||
threads: this._threads.array.filter(x=>x).map(t => {
|
||||
var javaid = parseInt(t.threadid, 16);
|
||||
@@ -702,7 +746,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
};
|
||||
this.sendResponse(response);
|
||||
})
|
||||
.fail(e => {
|
||||
.fail(() => {
|
||||
response.success = false;
|
||||
this.sendResponse(response);
|
||||
});
|
||||
@@ -716,7 +760,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
// debugger threadid's are a padded 64bit hex string
|
||||
var thread = this.getThread(args.threadId);
|
||||
if (!thread) return this.failRequestNoThread('Stack trace', args.threadId, response);
|
||||
if (!thread.paused) return this.failRequestThreadNotSuspended('Stack trace', args.threadId, response);
|
||||
if (!thread.paused) return this.cancelRequestThreadNotSuspended('Stack trace', args.threadId, response);
|
||||
|
||||
// retrieve the (stack) frames from the debugger
|
||||
this.dbgr.getframes(thread.threadid, {response, args, thread})
|
||||
@@ -732,8 +776,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
const endFrame = Math.min(startFrame + maxLevels, frames.length);
|
||||
var stack = [], totalFrames = frames.length, highest_known_source=0;
|
||||
const android_src_path = this._android_sources_path || '{Android SDK}';
|
||||
const device_api_level = this.dbgr.session.apilevel || '25';
|
||||
for (var i= startFrame; i < endFrame; i++) {
|
||||
for (var i = startFrame; (i < endFrame) && x.thread.paused; i++) {
|
||||
// the stack_frame_id must be unique across all threads
|
||||
const stack_frame_id = x.thread.addStackFrameVariable(frames[i], i).frameId;
|
||||
const name = `${frames[i].method.owningclass.name}.${frames[i].method.name}`;
|
||||
@@ -767,7 +810,10 @@ class AndroidDebugSession extends DebugSession {
|
||||
: null;
|
||||
const src = new Source(sourcefile, srcpath, srcpath ? 0 : srcRefId);
|
||||
pkginfo && (highest_known_source=i);
|
||||
stack.push(new StackFrame(stack_frame_id, name, src, linenum, 0));
|
||||
// we don't support column number when reporting source locations (because JDWP only supports line-granularity)
|
||||
// but in order to get the Exception UI to show, we must have a non-zero column
|
||||
const colnum = (!i && x.thread.paused.last_exception && x.thread.paused.reasons[0]==='exception') ? 1 : 0;
|
||||
stack.push(new StackFrame(stack_frame_id, name, src, linenum, colnum));
|
||||
}
|
||||
// trim the stack to exclude calls above the known sources
|
||||
if (this.callStackDisplaySize > 0) {
|
||||
@@ -781,7 +827,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
};
|
||||
this.sendResponse(response);
|
||||
})
|
||||
.fail((e,x) => {
|
||||
.fail(() => {
|
||||
this.failRequest('No call stack is available', response);
|
||||
});
|
||||
}
|
||||
@@ -790,7 +836,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
var threadId = variableRefToThreadId(args.frameId);
|
||||
var thread = this.getThread(threadId);
|
||||
if (!thread) return this.failRequestNoThread('Scopes',threadId, response);
|
||||
if (!thread.paused) return this.failRequestThreadNotSuspended('Scopes',threadId, response);
|
||||
if (!thread.paused) return this.cancelRequestThreadNotSuspended('Scopes', threadId, response);
|
||||
|
||||
var scopes = [new Scope("Local", args.frameId, false)];
|
||||
response.body = {
|
||||
@@ -801,21 +847,30 @@ class AndroidDebugSession extends DebugSession {
|
||||
if (last_exception && !last_exception.objvar) {
|
||||
// retrieve the exception object
|
||||
thread.allocateExceptionScopeReference(args.frameId);
|
||||
this.dbgr.getExceptionLocal(last_exception.exception, {response,scopes,last_exception})
|
||||
this.dbgr.getExceptionLocal(last_exception.exception, {thread,response,scopes,last_exception})
|
||||
.then((ex_local,x) => {
|
||||
var {response,scopes,last_exception} = x;
|
||||
last_exception.objvar = ex_local;
|
||||
// put the exception first - otherwise it can get lost if there's a lot of locals
|
||||
scopes.unshift(new Scope("Exception: "+ex_local.type.typename, last_exception.scopeRef, false));
|
||||
this.sendResponse(response);
|
||||
x.last_exception.objvar = ex_local;
|
||||
return $.when(x, x.thread.getVariables(x.last_exception.scopeRef));
|
||||
})
|
||||
.fail(e => { this.sendResponse(response); });
|
||||
.then((x, vars) => {
|
||||
var {response,scopes,last_exception} = x;
|
||||
// put the exception first - otherwise it can get lost if there's a lot of locals
|
||||
scopes.unshift(new Scope("Exception: " + last_exception.objvar.type.typename, last_exception.scopeRef, false));
|
||||
this.sendResponse(response);
|
||||
// notify the exceptionInfo who may be waiting on us
|
||||
if (last_exception.waitForExObject) {
|
||||
var def = last_exception.waitForExObject;
|
||||
last_exception.waitForExObject = null;
|
||||
def.resolveWith(this, []);
|
||||
}
|
||||
})
|
||||
.fail((/*e*/) => { this.sendResponse(response); });
|
||||
return;
|
||||
}
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
sourceRequest(response/*: DebugProtocol.SourceResponse*/, args/*: DebugProtocol.SourceArguments*/) {
|
||||
sourceRequest(response/*: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments*/) {
|
||||
var content =
|
||||
`/*
|
||||
The source for this class is unavailable.
|
||||
@@ -844,7 +899,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
var threadId = variableRefToThreadId(args.variablesReference);
|
||||
var thread = this.getThread(threadId);
|
||||
if (!thread) return this.failRequestNoThread('Variables',threadId, response);
|
||||
if (!thread.paused) return this.failRequestThreadNotSuspended('Variables',threadId, response);
|
||||
if (!thread.paused) return this.cancelRequestThreadNotSuspended('Variables',threadId, response);
|
||||
|
||||
thread.getVariables(args.variablesReference)
|
||||
.then(vars => {
|
||||
@@ -877,7 +932,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
return;
|
||||
}
|
||||
}
|
||||
var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid);
|
||||
var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid, thread.paused.last_exception && "Exception thrown");
|
||||
thread.paused.stoppedEvent = event;
|
||||
this.sendEvent(event);
|
||||
}
|
||||
@@ -926,7 +981,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.sendResponse(response);
|
||||
// we time the step - if it takes more than 2 seconds, we switch to any other threads that are waiting
|
||||
t.stepTimeout = setTimeout(t => {
|
||||
console.log('Step timeout on thread:'+t.threadid);
|
||||
D('Step timeout on thread:'+t.threadid);
|
||||
t.stepTimeout = null;
|
||||
this.checkPendingThreadBreaks();
|
||||
}, 2000, t);
|
||||
@@ -1070,11 +1125,11 @@ class AndroidDebugSession extends DebugSession {
|
||||
if (!this._evals_queue.length) {
|
||||
return;
|
||||
}
|
||||
var {response, args, getvars, thread} = this._evals_queue[0];
|
||||
var {response, args, getvars} = this._evals_queue[0];
|
||||
|
||||
// wait for any locals in the given context to be retrieved
|
||||
getvars.then((thread, locals, vars) => {
|
||||
return this.evaluate(args.expression, thread, locals, vars);
|
||||
return evaluate(args.expression, thread, locals, vars, this.dbgr);
|
||||
})
|
||||
.then((value,variablesReference) => {
|
||||
response.body = { result:value, variablesReference:variablesReference|0 };
|
||||
@@ -1090,479 +1145,52 @@ class AndroidDebugSession extends DebugSession {
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Asynchronously evaluate an expression
|
||||
*/
|
||||
evaluate(expression, thread, locals, vars) {
|
||||
D('evaluate: ' + expression);
|
||||
exceptionInfoRequest(response /*DebugProtocol.ExceptionInfoResponse*/, args /**/) {
|
||||
var thread = this.getThread(args.threadId);
|
||||
if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response);
|
||||
if (!thread.paused) return this.cancelRequestThreadNotSuspended('Exception info', args.threadId, response);
|
||||
if (!thread.paused.last_exception) return this.failRequest('No exception available', response);
|
||||
|
||||
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
|
||||
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]);
|
||||
|
||||
if (thread && !thread.paused)
|
||||
return reject_evaluation('not available');
|
||||
|
||||
// special case for evaluating exception messages
|
||||
// - this is called if the user tries to evaluate ':msg' from the locals
|
||||
if (expression===exmsg_var_name) {
|
||||
if (thread && thread.paused.last_exception && thread.paused.last_exception.cached) {
|
||||
var msglocal = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
|
||||
if (msglocal) {
|
||||
return resolve_evaluation(vars._local_to_variable(msglocal).value);
|
||||
}
|
||||
}
|
||||
return reject_evaluation('not available');
|
||||
}
|
||||
|
||||
const parse_array_or_fncall = function(e) {
|
||||
var arg, res = {arr:[], call:null};
|
||||
// pre-call array indexes
|
||||
while (e.expr[0] === '[') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
if ((arg = parse_expression(e)) === null) return null;
|
||||
res.arr.push(arg);
|
||||
if (e.expr[0] !== ']') return null;
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
}
|
||||
if (res.arr.length) return res;
|
||||
// method call
|
||||
if (e.expr[0] === '(') {
|
||||
res.call = []; e.expr = e.expr.slice(1).trim();
|
||||
if (e.expr[0] !== ')') {
|
||||
for (;;) {
|
||||
if ((arg = parse_expression(e)) === null) return null;
|
||||
res.call.push(arg);
|
||||
if (e.expr[0] === ')') break;
|
||||
if (e.expr[0] !== ',') return null;
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
}
|
||||
}
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
// post-call array indexes
|
||||
while (e.expr[0] === '[') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
if ((arg = parse_expression(e)) === null) return null;
|
||||
res.arr.push(arg);
|
||||
if (e.expr[0] !== ']') return null;
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const parse_expression_term = function(e) {
|
||||
if (e.expr[0] === '(') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
var subexpr = {expr:e.expr};
|
||||
var res = parse_expression(subexpr);
|
||||
if (res) {
|
||||
if (subexpr.expr[0] !== ')') return null;
|
||||
e.expr = subexpr.expr.slice(1).trim();
|
||||
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.members.length && !res.array_or_fncall.call && !res.array_or_fncall.arr.length) {
|
||||
// primitive typecast
|
||||
var castexpr = parse_expression_term(e);
|
||||
if (castexpr) castexpr.typecast = res.root_term;
|
||||
res = castexpr;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
var unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
|
||||
if (unop) {
|
||||
var op = unop[0].replace(/\s/g,'');
|
||||
e.expr = e.expr.slice(unop[0].length).trim();
|
||||
var res = parse_expression_term(e);
|
||||
if (res) {
|
||||
for (var i=op.length-1; i >= 0; --i)
|
||||
res = { operator:op[i], rhs:res };
|
||||
}
|
||||
return res;
|
||||
}
|
||||
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.\d+(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
||||
if (!root_term) return null;
|
||||
var res = {
|
||||
root_term: root_term[0],
|
||||
root_term_type: ['boolean','boolean','null','ident','hexint','octint','decfloat','decint','char','echar','uchar','string'][[1,2,3,4,5,6,7,8,9,10,11,12].find(x => root_term[x])-1],
|
||||
array_or_fncall: null,
|
||||
members:[],
|
||||
typecast:''
|
||||
}
|
||||
e.expr = e.expr.slice(res.root_term.length).trim();
|
||||
if ((res.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
||||
// the root term is not allowed to be a method call
|
||||
if (res.array_or_fncall.call) return null;
|
||||
while (e.expr[0] === '.') {
|
||||
// member expression
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
var m, member_name = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
|
||||
if (!member_name) return null;
|
||||
res.members.push(m = {member:member_name[0], array_or_fncall:null})
|
||||
e.expr = e.expr.slice(m.member.length).trim();
|
||||
if ((m.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const prec = {
|
||||
'*':1,'%':1,'/':1,
|
||||
'+':2,'-':2,
|
||||
'<<':3,'>>':3,'>>>':3,
|
||||
'<':4,'>':4,'<=':4,'>=':4,'instanceof':4,
|
||||
'==':5,'!=':5,
|
||||
'&':6,'^':7,'|':8,'&&':9,'||':10,'?':11,
|
||||
}
|
||||
const parse_expression = function(e) {
|
||||
var res = parse_expression_term(e);
|
||||
|
||||
if (!e.currprec) e.currprec = [12];
|
||||
for (;;) {
|
||||
var binary_operator = e.expr.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/ );
|
||||
if (!binary_operator) break;
|
||||
var precdiff = (prec[binary_operator[0]] || 12) - e.currprec[0];
|
||||
if (precdiff > 0) {
|
||||
// bigger number -> lower precendence -> end of (sub)expression
|
||||
break;
|
||||
}
|
||||
if (precdiff === 0 && binary_operator[0]!=='?') {
|
||||
// equal precedence, ltr evaluation
|
||||
break;
|
||||
}
|
||||
// higher or equal precendence
|
||||
e.currprec.unshift(e.currprec[0]+precdiff);
|
||||
e.expr = e.expr.slice(binary_operator[0].length).trim();
|
||||
// current or higher precendence
|
||||
if (binary_operator[0]==='?') {
|
||||
res = { condition:res, operator:binary_operator[0], ternary_true:null, ternary_false:null };
|
||||
res.ternary_true = parse_expression(o);
|
||||
symbol(e,':');
|
||||
res.ternary_false = parse_expression(o);
|
||||
} else {
|
||||
res = { lhs:res, operator:binary_operator[0], rhs:parse_expression(e) };
|
||||
}
|
||||
e.currprec.shift();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const hex_long = long => ('000000000000000' + long.toUnsigned().toString(16)).slice(-16);
|
||||
const evaluate_number = (n) => {
|
||||
n += '';
|
||||
var numtype,m = n.match(/^([+-]?)0([bBxX0-7])(.+)/), base=10;
|
||||
if (m) {
|
||||
switch(m[2]) {
|
||||
case 'b': base = 2; n = m[1]+m[3]; break;
|
||||
case 'x': base = 16; n = m[1]+m[3]; break;
|
||||
default: base = 8; break;
|
||||
}
|
||||
}
|
||||
if (base !== 16 && /[fFdD]$/.test(n)) {
|
||||
numtype = /[fF]$/.test(n) ? 'float' : 'double';
|
||||
n = n.slice(0,-1);
|
||||
} else if (/[lL]$/.test(n)) {
|
||||
numtype = 'long'
|
||||
n = n.slice(0,-1);
|
||||
} else {
|
||||
numtype = /\./.test(n) ? 'double' : 'int';
|
||||
}
|
||||
if (numtype === 'long') n = hex_long(Long.fromString(n, false, base));
|
||||
else if (/^[fd]/.test(numtype)) n = (base === 10) ? parseFloat(n) : parseInt(n, base);
|
||||
else n = parseInt(n, base)|0;
|
||||
|
||||
const iszero = /^[+-]?0+(\.0*)?$/.test(n);
|
||||
return { vtype:'literal',name:'',hasnullvalue:iszero,type:JTYPES[numtype],value:n,valid:true };
|
||||
}
|
||||
const evaluate_char = (char) => {
|
||||
return { vtype:'literal',name:'',char:char,hasnullvalue:false,type:JTYPES.char,value:char.charCodeAt(0),valid:true };
|
||||
}
|
||||
const numberify = (local) => {
|
||||
//if (local.type.signature==='C') return local.char.charCodeAt(0);
|
||||
if (/^[FD]$/.test(local.type.signature))
|
||||
return parseFloat(local.value);
|
||||
if (local.type.signature==='J')
|
||||
return parseInt(local.value,16);
|
||||
return parseInt(local.value,10);
|
||||
}
|
||||
const stringify = (local) => {
|
||||
var s;
|
||||
if (JTYPES.isString(local.type)) s = local.string;
|
||||
else if (JTYPES.isChar(local.type)) s= local.char;
|
||||
else if (JTYPES.isPrimitive(local.type)) s = ''+local.value;
|
||||
else if (local.hasnullvalue) s= '(null)';
|
||||
if (typeof s === 'string')
|
||||
return $.Deferred().resolveWith(this,[s]);
|
||||
return this.dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature)
|
||||
.then(s => s.string);
|
||||
}
|
||||
const evaluate_expression = (expr) => {
|
||||
var q = $.Deferred(), local;
|
||||
if (expr.operator) {
|
||||
const invalid_operator = (unary) => reject_evaluation(`Invalid ${unary?'type':'types'} for operator '${expr.operator}'`),
|
||||
divide_by_zero = () => reject_evaluation('ArithmeticException: divide by zero');
|
||||
var lhs_local;
|
||||
return !expr.lhs
|
||||
? // unary operator
|
||||
evaluate_expression(expr.rhs)
|
||||
.then(rhs_local => {
|
||||
if (expr.operator === '!' && JTYPES.isBoolean(rhs_local.type)) {
|
||||
rhs_local.value = !rhs_local.value;
|
||||
return rhs_local;
|
||||
}
|
||||
else if (expr.operator === '~' && JTYPES.isInteger(rhs_local.type)) {
|
||||
switch (rhs_local.type.typename) {
|
||||
case 'long': rhs_local.value = rhs_local.value.replace(/./g,c => (15 - parseInt(c,16)).toString(16)); break;
|
||||
default: rhs_local = evaluate_number(''+~rhs_local.value); break;
|
||||
}
|
||||
return rhs_local;
|
||||
}
|
||||
else if (/[+-]/.test(expr.operator) && JTYPES.isInteger(rhs_local.type)) {
|
||||
if (expr.operator === '+') return rhs_local;
|
||||
switch (rhs_local.type.typename) {
|
||||
case 'long': rhs_local.value = hex_long(Long.fromString(rhs_local.value,false,16).neg()); break;
|
||||
default: rhs_local = evaluate_number(''+(-rhs_local.value)); break;
|
||||
}
|
||||
return rhs_local;
|
||||
}
|
||||
return invalid_operator('unary');
|
||||
})
|
||||
: // binary operator
|
||||
evaluate_expression(expr.lhs)
|
||||
.then(x => (lhs_local = x) && evaluate_expression(expr.rhs))
|
||||
.then(rhs_local => {
|
||||
if ((lhs_local.type.signature==='J' && JTYPES.isInteger(rhs_local.type))
|
||||
|| (rhs_local.type.signature==='J' && JTYPES.isInteger(lhs_local.type))) {
|
||||
// one operand is a long, the other is an integer -> the result is a long
|
||||
var a,b, lbase, rbase;
|
||||
lbase = lhs_local.type.signature==='J' ? 16 : 10;
|
||||
rbase = rhs_local.type.signature==='J' ? 16 : 10;
|
||||
a = Long.fromString(''+lhs_local.value, false, lbase);
|
||||
b = Long.fromString(''+rhs_local.value, false, rbase);
|
||||
switch(expr.operator) {
|
||||
case '+': a = a.add(b); break;
|
||||
case '-': a = a.subtract(b); break;
|
||||
case '*': a = a.multiply(b); break;
|
||||
case '/': if (!b.isZero()) { a = a.divide(b); break } return divide_by_zero();
|
||||
case '%': if (!b.isZero()) { a = a.mod(b); break; } return divide_by_zero();
|
||||
case '<<': a = a.shl(b); break;
|
||||
case '>>': a = a.shr(b); break;
|
||||
case '>>>': a = a.shru(b); break;
|
||||
case '&': a = a.and(b); break;
|
||||
case '|': a = a.or(b); break;
|
||||
case '^': a = a.xor(b); break;
|
||||
case '==': a = a.eq(b); break;
|
||||
case '!=': a = !a.eq(b); break;
|
||||
case '<': a = a.lt(b); break;
|
||||
case '<=': a = a.lte(b); break;
|
||||
case '>': a = a.gt(b); break;
|
||||
case '>=': a = a.gte(b); break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
if (typeof a === 'boolean')
|
||||
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:a,valid:true };
|
||||
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.long,value:hex_long(a),valid:true };
|
||||
}
|
||||
else if (JTYPES.isInteger(lhs_local.type) && JTYPES.isInteger(rhs_local.type)) {
|
||||
// both are (non-long) integer types
|
||||
var a = numberify(lhs_local), b= numberify(rhs_local);
|
||||
switch(expr.operator) {
|
||||
case '+': a += b; break;
|
||||
case '-': a -= b; break;
|
||||
case '*': a *= b; break;
|
||||
case '/': if (b) { a = Math.trunc(a/b); break } return divide_by_zero();
|
||||
case '%': if (b) { a %= b; break; } return divide_by_zero();
|
||||
case '<<': a<<=b; break;
|
||||
case '>>': a>>=b; break;
|
||||
case '>>>': a>>>=b; break;
|
||||
case '&': a&=b; break;
|
||||
case '|': a|=b; break;
|
||||
case '^': a^=b; break;
|
||||
case '==': a = a===b; break;
|
||||
case '!=': a = a!==b; break;
|
||||
case '<': a = a<b; break;
|
||||
case '<=': a = a<=b; break;
|
||||
case '>': a = a>b; break;
|
||||
case '>=': a = a>=b; break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
if (typeof a === 'boolean')
|
||||
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:a,valid:true };
|
||||
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.int,value:''+a,valid:true };
|
||||
}
|
||||
else if (JTYPES.isNumber(lhs_local.type) && JTYPES.isNumber(rhs_local.type)) {
|
||||
var a = numberify(lhs_local), b= numberify(rhs_local);
|
||||
switch(expr.operator) {
|
||||
case '+': a += b; break;
|
||||
case '-': a -= b; break;
|
||||
case '*': a *= b; break;
|
||||
case '/': a /= b; break;
|
||||
case '==': a = a===b; break;
|
||||
case '!=': a = a!==b; break;
|
||||
case '<': a = a<b; break;
|
||||
case '<=': a = a<=b; break;
|
||||
case '>': a = a>b; break;
|
||||
case '>=': a = a>=b; break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
if (typeof a === 'boolean')
|
||||
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:a,valid:true };
|
||||
// one of them must be a float or double
|
||||
var result_type = 'float double'.split(' ')[Math.max("FD".indexOf(lhs_local.type.signature), "FD".indexOf(rhs_local.type.signature))];
|
||||
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES[result_type],value:''+a,valid:true };
|
||||
}
|
||||
else if (lhs_local.type.signature==='Z' && rhs_local.type.signature==='Z') {
|
||||
// boolean operands
|
||||
var a = lhs_local.value, b = rhs_local.value;
|
||||
switch(expr.operator) {
|
||||
case '&': case '&&': a = a && b; break;
|
||||
case '|': case '||': a = a || b; break;
|
||||
case '^': a = !!(a ^ b); break;
|
||||
case '==': a = a===b; break;
|
||||
case '!=': a = a!==b; break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:a,valid:true };
|
||||
}
|
||||
else if (expr.operator === '+' && JTYPES.isString(lhs_local.type)) {
|
||||
return stringify(rhs_local).then(rhs_str => createJavaString(this.dbgr, lhs_local.string + rhs_str,{israw:true}));
|
||||
}
|
||||
return invalid_operator();
|
||||
});
|
||||
}
|
||||
switch(expr.root_term_type) {
|
||||
case 'boolean':
|
||||
local = { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:expr.root_term!=='false',valid:true };
|
||||
break;
|
||||
case 'null':
|
||||
const nullvalue = '0000000000000000'; // null reference value
|
||||
local = { vtype:'literal',name:'',hasnullvalue:true,type:JTYPES.null,value:nullvalue,valid:true };
|
||||
break;
|
||||
case 'ident':
|
||||
local = locals && locals.find(l => l.name === expr.root_term);
|
||||
break;
|
||||
case 'hexint':
|
||||
case 'octint':
|
||||
case 'decint':
|
||||
case 'decfloat':
|
||||
local = evaluate_number(expr.root_term);
|
||||
break;
|
||||
case 'char':
|
||||
case 'echar':
|
||||
case 'uchar':
|
||||
local = evaluate_char(decode_char(expr.root_term.slice(1,-1)))
|
||||
break;
|
||||
case 'string':
|
||||
// we must get the runtime to create string instances
|
||||
q = createJavaString(this.dbgr, expr.root_term);
|
||||
local = {valid:true}; // make sure we don't fail the evaluation
|
||||
break;
|
||||
}
|
||||
if (!local || !local.valid) return reject_evaluation('not available');
|
||||
// we've got the root term variable - work out the rest
|
||||
q = expr.array_or_fncall.arr.reduce((q,index_expr) => {
|
||||
return q.then(function(index_expr,local) { return evaluate_array_element.call(this,index_expr,local) }.bind(this,index_expr));
|
||||
}, q);
|
||||
q = expr.members.reduce((q,m) => {
|
||||
return q.then(function(m,local) { return evaluate_member.call(this,m,local) }.bind(this,m));
|
||||
}, q);
|
||||
if (expr.typecast) {
|
||||
q = q.then(function(type,local) { return evaluate_cast.call(this,type,local) }.bind(this,expr.typecast) )
|
||||
}
|
||||
// if it's a string literal, we are already waiting for the runtime to create the string
|
||||
// - otherwise, start the evalaution...
|
||||
if (expr.root_term_type !== 'string')
|
||||
q.resolveWith(this,[local]);
|
||||
return q;
|
||||
}
|
||||
const evaluate_array_element = (index_expr, arr_local) => {
|
||||
if (arr_local.type.signature[0] !== '[') return reject_evaluation(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`);
|
||||
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||
return evaluate_expression(index_expr)
|
||||
.then(function(arr_local, idx_local) {
|
||||
if (!JTYPES.isInteger(idx_local.type)) return reject_evaluation('TypeError: array index is not an integer value');
|
||||
var idx = numberify(idx_local);
|
||||
if (idx < 0 || idx >= arr_local.arraylen) return reject_evaluation(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`);
|
||||
return this.dbgr.getarrayvalues(arr_local, idx, 1)
|
||||
}.bind(this,arr_local))
|
||||
.then(els => els[0])
|
||||
}
|
||||
const evaluate_methodcall = (m, obj_local) => {
|
||||
return reject_evaluation('Error: method calls are not supported');
|
||||
}
|
||||
const evaluate_member = (m, obj_local) => {
|
||||
if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type');
|
||||
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||
var chain;
|
||||
if (m.array_or_fncall.call){
|
||||
chain = evaluate_methodcall(m, obj_local);
|
||||
}
|
||||
// length is a 'fake' field of arrays, so special-case it
|
||||
else if (JTYPES.isArray(obj_local.type) && m.member==='length') {
|
||||
chain = evaluate_number(obj_local.arraylen);
|
||||
}
|
||||
// we also special-case :super (for object instances)
|
||||
else if (JTYPES.isObject(obj_local.type) && m.member === ':super') {
|
||||
chain = this.dbgr.getsuperinstance(obj_local);
|
||||
}
|
||||
// anything else must be a real field
|
||||
else {
|
||||
chain = this.dbgr.getFieldValue(obj_local, m.member, true)
|
||||
}
|
||||
|
||||
return chain.then(local => {
|
||||
if (m.array_or_fncall.arr.length) {
|
||||
var q = $.Deferred();
|
||||
m.array_or_fncall.arr.reduce((q,index_expr) => {
|
||||
return q.then(function(index_expr,local) { return evaluate_array_element(index_expr,local) }.bind(this,index_expr));
|
||||
}, q);
|
||||
return q.resolveWith(this, [local]);
|
||||
}
|
||||
});
|
||||
}
|
||||
const evaluate_cast = (type,local) => {
|
||||
if (type === local.type.typename) return local;
|
||||
const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`);
|
||||
// boolean cannot be converted from anything else
|
||||
if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast();
|
||||
if (local.type.typename === 'long') {
|
||||
// long to something else
|
||||
var value = Long.fromString(local.value, true, 16);
|
||||
switch(true) {
|
||||
case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2),16) << 24) >> 24); break;
|
||||
case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4),16) << 16) >> 16); break;
|
||||
case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8),16) | 0)); break;
|
||||
case (type === 'char'): local = evaluate_char(String.fromCharCode(parseInt(value.toString(16).slice(-4),16))); break;
|
||||
case (type === 'float'): local = evaluate_number(value.toSigned().toNumber()+'F'); break;
|
||||
case (type === 'double'): local = evaluate_number(value.toSigned().toNumber() + 'D'); break;
|
||||
default: return incompatible_cast();
|
||||
}
|
||||
} else {
|
||||
switch(true) {
|
||||
case (type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break;
|
||||
case (type === 'short'): local = evaluate_number((local.value << 16) >> 16); break;
|
||||
case (type === 'int'): local = evaluate_number((local.value | 0)); break;
|
||||
case (type === 'long'): local = evaluate_number(local.value+'L');break;
|
||||
case (type === 'char'): local = evaluate_char(String.fromCharCode(local.value|0)); break;
|
||||
case (type === 'float'): break;
|
||||
case (type === 'double'): break;
|
||||
default: return incompatible_cast();
|
||||
}
|
||||
}
|
||||
local.type = JTYPES[type];
|
||||
return local;
|
||||
}
|
||||
|
||||
var e = { expr:expression.trim() };
|
||||
var parsed_expression = parse_expression(e);
|
||||
// if there's anything left, it's an error
|
||||
if (parsed_expression && !e.expr) {
|
||||
// the expression is well-formed - start the (asynchronous) evaluation
|
||||
return evaluate_expression(parsed_expression)
|
||||
.then(local => {
|
||||
var v = vars._local_to_variable(local);
|
||||
return resolve_evaluation(v.value, v.variablesReference);
|
||||
if (!thread.paused.last_exception.objvar || !thread.paused.last_exception.cached) {
|
||||
// we must wait for the exception object to be retreived as a local (along with the message field)
|
||||
if (!thread.paused.last_exception.waitForExObject) {
|
||||
thread.paused.last_exception.waitForExObject = $.Deferred().then(() => {
|
||||
// redo the request
|
||||
this.exceptionInfoRequest(response, args);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
var exobj = thread.paused.last_exception.objvar;
|
||||
var exmsg = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
|
||||
exmsg = (exmsg && exmsg.string) || '';
|
||||
|
||||
// the expression is not well-formed
|
||||
return reject_evaluation('not available');
|
||||
}
|
||||
|
||||
response.body = {
|
||||
/** ID of the exception that was thrown. */
|
||||
exceptionId: exobj.type.typename,
|
||||
/** Descriptive text for the exception provided by the debug adapter. */
|
||||
description: exmsg,
|
||||
/** Mode that caused the exception notification to be raised. */
|
||||
//'never' | 'always' | 'unhandled' | 'userUnhandled';
|
||||
breakMode: 'always',
|
||||
/** Detailed information about the exception. */
|
||||
details: {
|
||||
/** Message contained in the exception. */
|
||||
message: exmsg,
|
||||
/** Short type name of the exception object. */
|
||||
typeName: exobj.type.typename,
|
||||
/** Fully-qualified type name of the exception object. */
|
||||
fullTypeName: signatureToFullyQualifiedType(exobj.type.signature),
|
||||
/** Optional expression that can be evaluated in the current scope to obtain the exception object. */
|
||||
//evaluateName: "evaluateName",
|
||||
/** Stack trace at the time the exception was thrown. */
|
||||
//stackTrace: "stackTrace",
|
||||
/** Details of the exception contained by this exception, if any. */
|
||||
//innerException: [],
|
||||
}
|
||||
}
|
||||
this.sendResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -525,7 +525,7 @@ Debugger.prototype = {
|
||||
},
|
||||
|
||||
_splitsrcfpn: function (srcfpn) {
|
||||
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.java$/);
|
||||
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.(java|kt)$/);
|
||||
return {
|
||||
pkg: m[1].replace(/\/+/g, '.'),
|
||||
type: m[2],
|
||||
@@ -1054,6 +1054,50 @@ Debugger.prototype = {
|
||||
return this.invokeMethod(objectid, threadid, type_signature || 'Ljava/lang/Object;', 'toString', '()Ljava/lang/String;', [], extra);
|
||||
},
|
||||
|
||||
findNamedMethods(type_signature, name, method_signature) {
|
||||
var x = { type_signature, name, method_signature }
|
||||
const ismatch = function(x, y) {
|
||||
if (!x || (x === y)) return true;
|
||||
return (x instanceof RegExp) && x.test(y);
|
||||
}
|
||||
return this.gettypedebuginfo(x.type_signature)
|
||||
.then(dbgtype => this._ensuremethods(dbgtype[x.type_signature]))
|
||||
.then(typeinfo => ({
|
||||
// resolving the methods only resolves the non-inherited methods
|
||||
// if we can't find a matching method, we need to search the super types
|
||||
dbgr: this,
|
||||
def: $.Deferred(),
|
||||
matches:[],
|
||||
find_methods(typeinfo) {
|
||||
for (var mid in typeinfo.methods) {
|
||||
var m = typeinfo.methods[mid];
|
||||
// does the name match
|
||||
if (!ismatch(x.name, m.name)) continue;
|
||||
// does the signature match
|
||||
if (!ismatch(x.method_signature, m.genericsig || m.sig)) continue;
|
||||
// add it to the results
|
||||
this.matches.push(m);
|
||||
}
|
||||
// search the supertype
|
||||
if (typeinfo.type.signature === 'Ljava/lang/Object;') {
|
||||
this.def.resolveWith(this.dbgr, [this.matches]);
|
||||
return this;
|
||||
}
|
||||
this.dbgr._ensuresuper(typeinfo)
|
||||
.then(typeinfo => {
|
||||
return this.dbgr.gettypedebuginfo(typeinfo.super.signature, typeinfo.super.signature)
|
||||
})
|
||||
.then((dbgtype, sig) => {
|
||||
return this.dbgr._ensuremethods(dbgtype[sig])
|
||||
})
|
||||
.then(typeinfo => {
|
||||
this.find_methods(typeinfo)
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}).find_methods(typeinfo).def)
|
||||
},
|
||||
|
||||
getstringchars: function (stringref, extra) {
|
||||
return this.session.adbclient.jdwp_command({
|
||||
ths: this,
|
||||
@@ -1269,8 +1313,10 @@ Debugger.prototype = {
|
||||
return $.Deferred().resolveWith(this, [typeinfo]);
|
||||
}
|
||||
if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') {
|
||||
typeinfo.super = null;
|
||||
return $.Deferred().resolveWith(this, [typeinfo]);
|
||||
if (typeinfo.info.reftype.string !== 'array') {
|
||||
typeinfo.super = null;
|
||||
return $.Deferred().resolveWith(this, [typeinfo]);
|
||||
}
|
||||
}
|
||||
|
||||
typeinfo.super = $.Deferred();
|
||||
@@ -1379,6 +1425,15 @@ Debugger.prototype = {
|
||||
cmd: this.JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo),
|
||||
})
|
||||
.then(function (linetable, methodinfo) {
|
||||
// if the request failed, just return a blank table
|
||||
if (linetable.errorcode) {
|
||||
linetable = {
|
||||
errorcode: linetable.errorcode,
|
||||
start: '00000000000000000000000000000000',
|
||||
end: '00000000000000000000000000000000',
|
||||
lines:[],
|
||||
}
|
||||
}
|
||||
// the linetable does not correlate code indexes with line numbers
|
||||
// - location searching relies on the table being ordered by code indexes
|
||||
linetable.lines.sort(function (a, b) {
|
||||
|
||||
535
src/expressions.js
Normal file
535
src/expressions.js
Normal file
@@ -0,0 +1,535 @@
|
||||
'use strict'
|
||||
const Long = require('long');
|
||||
const $ = require('./jq-promise');
|
||||
const { D } = require('./util');
|
||||
const { JTYPES, exmsg_var_name, decode_char, createJavaString } = require('./globals');
|
||||
|
||||
/*
|
||||
Asynchronously evaluate an expression
|
||||
*/
|
||||
exports.evaluate = function(expression, thread, locals, vars, dbgr) {
|
||||
D('evaluate: ' + expression);
|
||||
|
||||
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
|
||||
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]);
|
||||
|
||||
if (thread && !thread.paused)
|
||||
return reject_evaluation('not available');
|
||||
|
||||
// special case for evaluating exception messages
|
||||
// - this is called if the user tries to evaluate ':msg' from the locals
|
||||
if (expression === exmsg_var_name) {
|
||||
if (thread && thread.paused.last_exception && thread.paused.last_exception.cached) {
|
||||
var msglocal = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
|
||||
if (msglocal) {
|
||||
return resolve_evaluation(vars._local_to_variable(msglocal).value);
|
||||
}
|
||||
}
|
||||
return reject_evaluation('not available');
|
||||
}
|
||||
|
||||
const parse_array_or_fncall = function (e) {
|
||||
var arg, res = { arr: [], call: null };
|
||||
// pre-call array indexes
|
||||
while (e.expr[0] === '[') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
if ((arg = parse_expression(e)) === null) return null;
|
||||
res.arr.push(arg);
|
||||
if (e.expr[0] !== ']') return null;
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
}
|
||||
if (res.arr.length) return res;
|
||||
// method call
|
||||
if (e.expr[0] === '(') {
|
||||
res.call = []; e.expr = e.expr.slice(1).trim();
|
||||
if (e.expr[0] !== ')') {
|
||||
for (; ;) {
|
||||
if ((arg = parse_expression(e)) === null) return null;
|
||||
res.call.push(arg);
|
||||
if (e.expr[0] === ')') break;
|
||||
if (e.expr[0] !== ',') return null;
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
}
|
||||
}
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
// post-call array indexes
|
||||
while (e.expr[0] === '[') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
if ((arg = parse_expression(e)) === null) return null;
|
||||
res.arr.push(arg);
|
||||
if (e.expr[0] !== ']') return null;
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const parse_expression_term = function (e) {
|
||||
if (e.expr[0] === '(') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
var subexpr = { expr: e.expr };
|
||||
var res = parse_expression(subexpr);
|
||||
if (res) {
|
||||
if (subexpr.expr[0] !== ')') return null;
|
||||
e.expr = subexpr.expr.slice(1).trim();
|
||||
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.members.length && !res.array_or_fncall.call && !res.array_or_fncall.arr.length) {
|
||||
// primitive typecast
|
||||
var castexpr = parse_expression_term(e);
|
||||
if (castexpr) castexpr.typecast = res.root_term;
|
||||
res = castexpr;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
var unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
|
||||
if (unop) {
|
||||
var op = unop[0].replace(/\s/g, '');
|
||||
e.expr = e.expr.slice(unop[0].length).trim();
|
||||
var res = parse_expression_term(e);
|
||||
if (res) {
|
||||
for (var i = op.length - 1; i >= 0; --i)
|
||||
res = { operator: op[i], rhs: res };
|
||||
}
|
||||
return res;
|
||||
}
|
||||
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.\d+(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
||||
if (!root_term) return null;
|
||||
var res = {
|
||||
root_term: root_term[0],
|
||||
root_term_type: ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'][[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1],
|
||||
array_or_fncall: null,
|
||||
members: [],
|
||||
typecast: ''
|
||||
}
|
||||
e.expr = e.expr.slice(res.root_term.length).trim();
|
||||
if ((res.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
||||
// the root term is not allowed to be a method call
|
||||
if (res.array_or_fncall.call) return null;
|
||||
while (e.expr[0] === '.') {
|
||||
// member expression
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
var m, member_name = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
|
||||
if (!member_name) return null;
|
||||
res.members.push(m = { member: member_name[0], array_or_fncall: null })
|
||||
e.expr = e.expr.slice(m.member.length).trim();
|
||||
if ((m.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const prec = {
|
||||
'*': 1, '%': 1, '/': 1,
|
||||
'+': 2, '-': 2,
|
||||
'<<': 3, '>>': 3, '>>>': 3,
|
||||
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
|
||||
'==': 5, '!=': 5,
|
||||
'&': 6, '^': 7, '|': 8, '&&': 9, '||': 10, '?': 11,
|
||||
}
|
||||
const parse_expression = function (e) {
|
||||
var res = parse_expression_term(e);
|
||||
|
||||
if (!e.currprec) e.currprec = [12];
|
||||
for (; ;) {
|
||||
var binary_operator = e.expr.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
|
||||
if (!binary_operator) break;
|
||||
var precdiff = (prec[binary_operator[0]] || 12) - e.currprec[0];
|
||||
if (precdiff > 0) {
|
||||
// bigger number -> lower precendence -> end of (sub)expression
|
||||
break;
|
||||
}
|
||||
if (precdiff === 0 && binary_operator[0] !== '?') {
|
||||
// equal precedence, ltr evaluation
|
||||
break;
|
||||
}
|
||||
// higher or equal precendence
|
||||
e.currprec.unshift(e.currprec[0] + precdiff);
|
||||
e.expr = e.expr.slice(binary_operator[0].length).trim();
|
||||
// current or higher precendence
|
||||
if (binary_operator[0] === '?') {
|
||||
res = { condition: res, operator: binary_operator[0], ternary_true: null, ternary_false: null };
|
||||
res.ternary_true = parse_expression(e);
|
||||
if (e.expr[0] === ':') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
res.ternary_false = parse_expression(e);
|
||||
}
|
||||
} else {
|
||||
res = { lhs: res, operator: binary_operator[0], rhs: parse_expression(e) };
|
||||
}
|
||||
e.currprec.shift();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const hex_long = long => ('000000000000000' + long.toUnsigned().toString(16)).slice(-16);
|
||||
const evaluate_number = (n) => {
|
||||
n += '';
|
||||
var numtype, m = n.match(/^([+-]?)0([bBxX0-7])(.+)/), base = 10;
|
||||
if (m) {
|
||||
switch (m[2]) {
|
||||
case 'b': base = 2; n = m[1] + m[3]; break;
|
||||
case 'x': base = 16; n = m[1] + m[3]; break;
|
||||
default: base = 8; break;
|
||||
}
|
||||
}
|
||||
if (base !== 16 && /[fFdD]$/.test(n)) {
|
||||
numtype = /[fF]$/.test(n) ? 'float' : 'double';
|
||||
n = n.slice(0, -1);
|
||||
} else if (/[lL]$/.test(n)) {
|
||||
numtype = 'long'
|
||||
n = n.slice(0, -1);
|
||||
} else {
|
||||
numtype = /\./.test(n) ? 'double' : 'int';
|
||||
}
|
||||
if (numtype === 'long') n = hex_long(Long.fromString(n, false, base));
|
||||
else if (/^[fd]/.test(numtype)) n = (base === 10) ? parseFloat(n) : parseInt(n, base);
|
||||
else n = parseInt(n, base) | 0;
|
||||
|
||||
const iszero = /^[+-]?0+(\.0*)?$/.test(n);
|
||||
return { vtype: 'literal', name: '', hasnullvalue: iszero, type: JTYPES[numtype], value: n, valid: true };
|
||||
}
|
||||
const evaluate_char = (char) => {
|
||||
return { vtype: 'literal', name: '', char: char, hasnullvalue: false, type: JTYPES.char, value: char.charCodeAt(0), valid: true };
|
||||
}
|
||||
const numberify = (local) => {
|
||||
//if (local.type.signature==='C') return local.char.charCodeAt(0);
|
||||
if (/^[FD]$/.test(local.type.signature))
|
||||
return parseFloat(local.value);
|
||||
if (local.type.signature === 'J')
|
||||
return parseInt(local.value, 16);
|
||||
return parseInt(local.value, 10);
|
||||
}
|
||||
const stringify = (local) => {
|
||||
var s;
|
||||
if (JTYPES.isString(local.type)) s = local.string;
|
||||
else if (JTYPES.isChar(local.type)) s = local.char;
|
||||
else if (JTYPES.isPrimitive(local.type)) s = '' + local.value;
|
||||
else if (local.hasnullvalue) s = '(null)';
|
||||
if (typeof s === 'string')
|
||||
return $.Deferred().resolveWith(this, [s]);
|
||||
return dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature)
|
||||
.then(s => s.string);
|
||||
}
|
||||
const evaluate_expression = (expr) => {
|
||||
var q = $.Deferred(), local;
|
||||
if (expr.operator) {
|
||||
const invalid_operator = (unary) => reject_evaluation(`Invalid ${unary ? 'type' : 'types'} for operator '${expr.operator}'`),
|
||||
divide_by_zero = () => reject_evaluation('ArithmeticException: divide by zero');
|
||||
var lhs_local;
|
||||
return !expr.lhs
|
||||
? // unary operator
|
||||
evaluate_expression(expr.rhs)
|
||||
.then(rhs_local => {
|
||||
if (expr.operator === '!' && JTYPES.isBoolean(rhs_local.type)) {
|
||||
rhs_local.value = !rhs_local.value;
|
||||
return rhs_local;
|
||||
}
|
||||
else if (expr.operator === '~' && JTYPES.isInteger(rhs_local.type)) {
|
||||
switch (rhs_local.type.typename) {
|
||||
case 'long': rhs_local.value = rhs_local.value.replace(/./g, c => (15 - parseInt(c, 16)).toString(16)); break;
|
||||
default: rhs_local = evaluate_number('' + ~rhs_local.value); break;
|
||||
}
|
||||
return rhs_local;
|
||||
}
|
||||
else if (/[+-]/.test(expr.operator) && JTYPES.isInteger(rhs_local.type)) {
|
||||
if (expr.operator === '+') return rhs_local;
|
||||
switch (rhs_local.type.typename) {
|
||||
case 'long': rhs_local.value = hex_long(Long.fromString(rhs_local.value, false, 16).neg()); break;
|
||||
default: rhs_local = evaluate_number('' + (-rhs_local.value)); break;
|
||||
}
|
||||
return rhs_local;
|
||||
}
|
||||
return invalid_operator('unary');
|
||||
})
|
||||
: // binary operator
|
||||
evaluate_expression(expr.lhs)
|
||||
.then(x => (lhs_local = x) && evaluate_expression(expr.rhs))
|
||||
.then(rhs_local => {
|
||||
if ((lhs_local.type.signature === 'J' && JTYPES.isInteger(rhs_local.type))
|
||||
|| (rhs_local.type.signature === 'J' && JTYPES.isInteger(lhs_local.type))) {
|
||||
// one operand is a long, the other is an integer -> the result is a long
|
||||
var a, b, lbase, rbase;
|
||||
lbase = lhs_local.type.signature === 'J' ? 16 : 10;
|
||||
rbase = rhs_local.type.signature === 'J' ? 16 : 10;
|
||||
a = Long.fromString('' + lhs_local.value, false, lbase);
|
||||
b = Long.fromString('' + rhs_local.value, false, rbase);
|
||||
switch (expr.operator) {
|
||||
case '+': a = a.add(b); break;
|
||||
case '-': a = a.subtract(b); break;
|
||||
case '*': a = a.multiply(b); break;
|
||||
case '/': if (!b.isZero()) { a = a.divide(b); break } return divide_by_zero();
|
||||
case '%': if (!b.isZero()) { a = a.mod(b); break; } return divide_by_zero();
|
||||
case '<<': a = a.shl(b); break;
|
||||
case '>>': a = a.shr(b); break;
|
||||
case '>>>': a = a.shru(b); break;
|
||||
case '&': a = a.and(b); break;
|
||||
case '|': a = a.or(b); break;
|
||||
case '^': a = a.xor(b); break;
|
||||
case '==': a = a.eq(b); break;
|
||||
case '!=': a = !a.eq(b); break;
|
||||
case '<': a = a.lt(b); break;
|
||||
case '<=': a = a.lte(b); break;
|
||||
case '>': a = a.gt(b); break;
|
||||
case '>=': a = a.gte(b); break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
if (typeof a === 'boolean')
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.long, value: hex_long(a), valid: true };
|
||||
}
|
||||
else if (JTYPES.isInteger(lhs_local.type) && JTYPES.isInteger(rhs_local.type)) {
|
||||
// both are (non-long) integer types
|
||||
var a = numberify(lhs_local), b = numberify(rhs_local);
|
||||
switch (expr.operator) {
|
||||
case '+': a += b; break;
|
||||
case '-': a -= b; break;
|
||||
case '*': a *= b; break;
|
||||
case '/': if (b) { a = Math.trunc(a / b); break } return divide_by_zero();
|
||||
case '%': if (b) { a %= b; break; } return divide_by_zero();
|
||||
case '<<': a <<= b; break;
|
||||
case '>>': a >>= b; break;
|
||||
case '>>>': a >>>= b; break;
|
||||
case '&': a &= b; break;
|
||||
case '|': a |= b; break;
|
||||
case '^': a ^= b; break;
|
||||
case '==': a = a === b; break;
|
||||
case '!=': a = a !== b; break;
|
||||
case '<': a = a < b; break;
|
||||
case '<=': a = a <= b; break;
|
||||
case '>': a = a > b; break;
|
||||
case '>=': a = a >= b; break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
if (typeof a === 'boolean')
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.int, value: '' + a, valid: true };
|
||||
}
|
||||
else if (JTYPES.isNumber(lhs_local.type) && JTYPES.isNumber(rhs_local.type)) {
|
||||
var a = numberify(lhs_local), b = numberify(rhs_local);
|
||||
switch (expr.operator) {
|
||||
case '+': a += b; break;
|
||||
case '-': a -= b; break;
|
||||
case '*': a *= b; break;
|
||||
case '/': a /= b; break;
|
||||
case '==': a = a === b; break;
|
||||
case '!=': a = a !== b; break;
|
||||
case '<': a = a < b; break;
|
||||
case '<=': a = a <= b; break;
|
||||
case '>': a = a > b; break;
|
||||
case '>=': a = a >= b; break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
if (typeof a === 'boolean')
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||
// one of them must be a float or double
|
||||
var result_type = 'float double'.split(' ')[Math.max("FD".indexOf(lhs_local.type.signature), "FD".indexOf(rhs_local.type.signature))];
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES[result_type], value: '' + a, valid: true };
|
||||
}
|
||||
else if (lhs_local.type.signature === 'Z' && rhs_local.type.signature === 'Z') {
|
||||
// boolean operands
|
||||
var a = lhs_local.value, b = rhs_local.value;
|
||||
switch (expr.operator) {
|
||||
case '&': case '&&': a = a && b; break;
|
||||
case '|': case '||': a = a || b; break;
|
||||
case '^': a = !!(a ^ b); break;
|
||||
case '==': a = a === b; break;
|
||||
case '!=': a = a !== b; break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||
}
|
||||
else if (expr.operator === '+' && JTYPES.isString(lhs_local.type)) {
|
||||
return stringify(rhs_local).then(rhs_str => createJavaString(dbgr, lhs_local.string + rhs_str, { israw: true }));
|
||||
}
|
||||
return invalid_operator();
|
||||
});
|
||||
}
|
||||
switch (expr.root_term_type) {
|
||||
case 'boolean':
|
||||
local = { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: expr.root_term !== 'false', valid: true };
|
||||
break;
|
||||
case 'null':
|
||||
const nullvalue = '0000000000000000'; // null reference value
|
||||
local = { vtype: 'literal', name: '', hasnullvalue: true, type: JTYPES.null, value: nullvalue, valid: true };
|
||||
break;
|
||||
case 'ident':
|
||||
local = locals && locals.find(l => l.name === expr.root_term);
|
||||
break;
|
||||
case 'hexint':
|
||||
case 'octint':
|
||||
case 'decint':
|
||||
case 'decfloat':
|
||||
local = evaluate_number(expr.root_term);
|
||||
break;
|
||||
case 'char':
|
||||
case 'echar':
|
||||
case 'uchar':
|
||||
local = evaluate_char(decode_char(expr.root_term.slice(1, -1)))
|
||||
break;
|
||||
case 'string':
|
||||
// we must get the runtime to create string instances
|
||||
q = createJavaString(dbgr, expr.root_term);
|
||||
local = { valid: true }; // make sure we don't fail the evaluation
|
||||
break;
|
||||
}
|
||||
if (!local || !local.valid) return reject_evaluation('not available');
|
||||
// we've got the root term variable - work out the rest
|
||||
q = expr.array_or_fncall.arr.reduce((q, index_expr) => {
|
||||
return q.then(function (index_expr, local) { return evaluate_array_element.call(this, index_expr, local) }.bind(this, index_expr));
|
||||
}, q);
|
||||
q = expr.members.reduce((q, m) => {
|
||||
return q.then(function (m, local) { return evaluate_member.call(this, m, local) }.bind(this, m));
|
||||
}, q);
|
||||
if (expr.typecast) {
|
||||
q = q.then(function (type, local) { return evaluate_cast.call(this, type, local) }.bind(this, expr.typecast))
|
||||
}
|
||||
// if it's a string literal, we are already waiting for the runtime to create the string
|
||||
// - otherwise, start the evalaution...
|
||||
if (expr.root_term_type !== 'string')
|
||||
q.resolveWith(this, [local]);
|
||||
return q;
|
||||
}
|
||||
const evaluate_array_element = (index_expr, arr_local) => {
|
||||
if (arr_local.type.signature[0] !== '[') return reject_evaluation(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`);
|
||||
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||
return evaluate_expression(index_expr)
|
||||
.then(function (arr_local, idx_local) {
|
||||
if (!JTYPES.isInteger(idx_local.type)) return reject_evaluation('TypeError: array index is not an integer value');
|
||||
var idx = numberify(idx_local);
|
||||
if (idx < 0 || idx >= arr_local.arraylen) return reject_evaluation(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`);
|
||||
return dbgr.getarrayvalues(arr_local, idx, 1)
|
||||
}.bind(this, arr_local))
|
||||
.then(els => els[0])
|
||||
}
|
||||
const evaluate_methodcall = (m, obj_local) => {
|
||||
// until we can figure out why method invokes with parameters crash the debugger, disallow parameterised calls
|
||||
if (m.array_or_fncall.call.length)
|
||||
return reject_evaluation('Error: method calls with parameter values are not supported');
|
||||
|
||||
// find any methods matching the member name with any parameters in the signature
|
||||
return dbgr.findNamedMethods(obj_local.type.signature, m.member, /^/)
|
||||
.then(methods => {
|
||||
if (!methods[0])
|
||||
return reject_evaluation(`Error: method '${m.member}()' not found`);
|
||||
// evaluate any parameters (and wait for the results)
|
||||
return $.when({methods},...m.array_or_fncall.call.map(evaluate_expression));
|
||||
})
|
||||
.then((x,...paramValues) => {
|
||||
// filter the method based upon the types of parameters - note that null types and integer literals can match multiple types
|
||||
paramValues = paramValues = paramValues.map(p => p[0]);
|
||||
var matchers = paramValues.map(p => {
|
||||
switch(true) {
|
||||
case p.type.signature === 'I':
|
||||
// match bytes/shorts/ints/longs/floats/doubles within range
|
||||
if (p.value >= -128 && p.value <= 127) return /^[BSIJFD]$/
|
||||
if (p.value >= -32768 && p.value <= 32767) return /^[SIJFD]$/
|
||||
return /^[IJFD]$/;
|
||||
case p.type.signature === 'F':
|
||||
return /^[FD]$/;
|
||||
case p.type.signature === 'Lnull;':
|
||||
return /^[LT\[]/; // any reference type
|
||||
default:
|
||||
// anything else must be an exact signature match (for now - in reality we should allow subclassed type)
|
||||
return new RegExp(`^${p.type.signature.replace(/[$]/g,x=>'\\'+x)}$`);
|
||||
}
|
||||
});
|
||||
var methods = x.methods.filter(m => {
|
||||
// extract a list of parameter types
|
||||
var paramtypere = /\[*([BSIJFDCZ]|([LT][^;]+;))/g;
|
||||
for (var x, ptypes=[]; x = paramtypere.exec(m.sig); ) {
|
||||
ptypes.push(x[0]);
|
||||
}
|
||||
// the last paramter type is the return value
|
||||
ptypes.pop();
|
||||
// check if they match
|
||||
if (ptypes.length !== paramValues.length)
|
||||
return;
|
||||
return matchers.filter(m => {
|
||||
return !m.test(ptypes.shift())
|
||||
}).length === 0;
|
||||
});
|
||||
if (!methods[0])
|
||||
return reject_evaluation(`Error: incompatible parameters for method '${m.member}'`);
|
||||
// convert the parameters to exact debugger-compatible values
|
||||
paramValues = paramValues.map(p => {
|
||||
if (p.type.signature.length === 1)
|
||||
return { type: p.type.typename, value: p.value};
|
||||
return { type: 'oref', value: p.value };
|
||||
})
|
||||
return dbgr.invokeMethod(obj_local.value, thread.threadid, obj_local.type.signature, m.member, methods[0].genericsig || methods[0].sig, paramValues, {});
|
||||
});
|
||||
}
|
||||
const evaluate_member = (m, obj_local) => {
|
||||
if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type');
|
||||
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||
var chain;
|
||||
if (m.array_or_fncall.call) {
|
||||
chain = evaluate_methodcall(m, obj_local);
|
||||
}
|
||||
// length is a 'fake' field of arrays, so special-case it
|
||||
else if (JTYPES.isArray(obj_local.type) && m.member === 'length') {
|
||||
chain = $.Deferred().resolve(evaluate_number(obj_local.arraylen));
|
||||
}
|
||||
// we also special-case :super (for object instances)
|
||||
else if (JTYPES.isObject(obj_local.type) && m.member === ':super') {
|
||||
chain = dbgr.getsuperinstance(obj_local);
|
||||
}
|
||||
// anything else must be a real field
|
||||
else {
|
||||
chain = dbgr.getFieldValue(obj_local, m.member, true)
|
||||
}
|
||||
|
||||
return chain.then(local => {
|
||||
if (m.array_or_fncall.arr.length) {
|
||||
var q = $.Deferred();
|
||||
m.array_or_fncall.arr.reduce((q, index_expr) => {
|
||||
return q.then(function (index_expr, local) { return evaluate_array_element(index_expr, local) }.bind(this, index_expr));
|
||||
}, q);
|
||||
return q.resolveWith(this, [local]);
|
||||
}
|
||||
});
|
||||
}
|
||||
const evaluate_cast = (type, local) => {
|
||||
if (type === local.type.typename) return local;
|
||||
const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`);
|
||||
// boolean cannot be converted from anything else
|
||||
if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast();
|
||||
if (local.type.typename === 'long') {
|
||||
// long to something else
|
||||
var value = Long.fromString(local.value, true, 16);
|
||||
switch (true) {
|
||||
case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2), 16) << 24) >> 24); break;
|
||||
case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4), 16) << 16) >> 16); break;
|
||||
case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8), 16) | 0)); break;
|
||||
case (type === 'char'): local = evaluate_char(String.fromCharCode(parseInt(value.toString(16).slice(-4), 16))); break;
|
||||
case (type === 'float'): local = evaluate_number(value.toSigned().toNumber() + 'F'); break;
|
||||
case (type === 'double'): local = evaluate_number(value.toSigned().toNumber() + 'D'); break;
|
||||
default: return incompatible_cast();
|
||||
}
|
||||
} else {
|
||||
switch (true) {
|
||||
case (type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break;
|
||||
case (type === 'short'): local = evaluate_number((local.value << 16) >> 16); break;
|
||||
case (type === 'int'): local = evaluate_number((local.value | 0)); break;
|
||||
case (type === 'long'): local = evaluate_number(local.value + 'L'); break;
|
||||
case (type === 'char'): local = evaluate_char(String.fromCharCode(local.value | 0)); break;
|
||||
case (type === 'float'): break;
|
||||
case (type === 'double'): break;
|
||||
default: return incompatible_cast();
|
||||
}
|
||||
}
|
||||
local.type = JTYPES[type];
|
||||
return local;
|
||||
}
|
||||
|
||||
var e = { expr: expression.trim() };
|
||||
var parsed_expression = parse_expression(e);
|
||||
// if there's anything left, it's an error
|
||||
if (parsed_expression && !e.expr) {
|
||||
// the expression is well-formed - start the (asynchronous) evaluation
|
||||
return evaluate_expression(parsed_expression)
|
||||
.then(local => {
|
||||
var v = vars._local_to_variable(local);
|
||||
return resolve_evaluation(v.value, v.variablesReference);
|
||||
});
|
||||
}
|
||||
|
||||
// the expression is not well-formed
|
||||
return reject_evaluation('not available');
|
||||
}
|
||||
@@ -27,6 +27,22 @@ const JTYPES = {
|
||||
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
|
||||
}
|
||||
|
||||
function signatureToFullyQualifiedType(sig) {
|
||||
var arr = sig.match(/^\[+/) || '';
|
||||
if (arr) {
|
||||
arr = '[]'.repeat(arr[0].length);
|
||||
sig = sig.slice(0, arr.length/2);
|
||||
}
|
||||
var m = sig.match(/^((L([^<;]+).)|T([^;]+).|.)/);
|
||||
if (!m) return '';
|
||||
if (m[3]) {
|
||||
return m[3].replace(/[/$]/g,'.') + arr;
|
||||
} else if (m[4]) {
|
||||
return m[4].replace(/[/$]/g, '.') + arr;
|
||||
}
|
||||
return JTYPES.fromPrimSig(sig[0]) + arr;
|
||||
}
|
||||
|
||||
// the special name given to exception message fields
|
||||
const exmsg_var_name = ':msg';
|
||||
|
||||
@@ -67,5 +83,5 @@ function variableRefToThreadId(variablesReference) {
|
||||
|
||||
|
||||
Object.assign(exports, {
|
||||
JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString
|
||||
JTYPES, exmsg_var_name, ensure_path_end_slash, is_subpath_of, decode_char, variableRefToThreadId, createJavaString, signatureToFullyQualifiedType
|
||||
});
|
||||
|
||||
15
src/jdwp.js
15
src/jdwp.js
@@ -1,5 +1,5 @@
|
||||
const $ = require('./jq-promise');
|
||||
const { atob,btoa,D,getutf8bytes,fromutf8bytes,intToHex } = require('./util');
|
||||
const { btoa,D,E,getutf8bytes,fromutf8bytes,intToHex } = require('./util');
|
||||
/*
|
||||
JDWP - The Java Debug Wire Protocol
|
||||
*/
|
||||
@@ -96,8 +96,8 @@ function _JDWP() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.errorcode != 0) {
|
||||
console.error("Command failed: error " + this.errorcode, this);
|
||||
if (this.errorcode !== 0) {
|
||||
E(`JDWP command failed '${this.command.name}'. Error ${this.errorcode}`, this);
|
||||
}
|
||||
|
||||
if (!this.errorcode && this.command && this.command.replydecodefn) {
|
||||
@@ -109,7 +109,10 @@ function _JDWP() {
|
||||
return;
|
||||
}
|
||||
|
||||
this.decoded = {empty:true};
|
||||
this.decoded = {
|
||||
empty: true,
|
||||
errorcode: this.errorcode,
|
||||
};
|
||||
}
|
||||
|
||||
this.decodereply = function(ths,s) {
|
||||
@@ -537,12 +540,12 @@ function _JDWP() {
|
||||
}
|
||||
m = signature.match(/^(\[+)(.+)$/);
|
||||
if (m) {
|
||||
var elementtype = this.signaturetotype(m[2]);
|
||||
var elementtype = this.signaturetotype(m[1].slice(0,-1) + m[2]);
|
||||
return {
|
||||
signature:signature,
|
||||
arraydims:m[1].length,
|
||||
elementtype: elementtype,
|
||||
typename:elementtype.typename+m[1].replace(/\[/g,'[]'),
|
||||
typename:elementtype.typename+'[]',
|
||||
}
|
||||
}
|
||||
var primitivetypes = {
|
||||
|
||||
@@ -40,7 +40,7 @@ var Deferred = exports.Deferred = function(p, parent) {
|
||||
var faildef = $.Deferred(null, this);
|
||||
var p = this._promise.catch(function(a) {
|
||||
if (a.stack) {
|
||||
console.error(a.stack);
|
||||
util.E(a.stack);
|
||||
a = [a];
|
||||
}
|
||||
if (this.def._context === null && this.def._parent)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
'use strict'
|
||||
// vscode stuff
|
||||
const { EventEmitter, Uri } = require('vscode');
|
||||
// node and external modules
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
@@ -17,10 +15,8 @@ const { D } = require('./util');
|
||||
*/
|
||||
class LogcatContent {
|
||||
|
||||
constructor(provider/*: AndroidContentProvider*/, uri/*: Uri*/) {
|
||||
this._provider = provider;
|
||||
this._uri = uri;
|
||||
this._logcatid = uri.query;
|
||||
constructor(deviceid) {
|
||||
this._logcatid = deviceid;
|
||||
this._logs = [];
|
||||
this._htmllogs = [];
|
||||
this._oldhtmllogs = [];
|
||||
@@ -29,7 +25,7 @@ class LogcatContent {
|
||||
this._refreshRate = 200; // ms
|
||||
this._state = '';
|
||||
this._htmltemplate = '';
|
||||
this._adbclient = new ADBClient(uri.query);
|
||||
this._adbclient = new ADBClient(deviceid);
|
||||
this._initwait = new Promise((resolve, reject) => {
|
||||
this._state = 'connecting';
|
||||
LogcatContent.initWebSocketServer()
|
||||
@@ -38,7 +34,7 @@ class LogcatContent {
|
||||
onlog: this.onLogcatContent.bind(this),
|
||||
onclose: this.onLogcatDisconnect.bind(this),
|
||||
});
|
||||
}).then(x => {
|
||||
}).then(() => {
|
||||
this._state = 'connected';
|
||||
this._initwait = null;
|
||||
resolve(this.content);
|
||||
@@ -55,20 +51,20 @@ class LogcatContent {
|
||||
return this.htmlBootstrap({connected:true, status:'',oldlogs:''});
|
||||
// if we're in the disconnected state, and this.content is called, it means the user has requested
|
||||
// this logcat again - check if the device has reconnected
|
||||
return this._initwait = new Promise((resolve, reject) => {
|
||||
return this._initwait = new Promise((resolve/*, reject*/) => {
|
||||
// clear the logs first - if we successfully reconnect, we will be retrieving the entire logcat again
|
||||
this._prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs };
|
||||
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
|
||||
this._adbclient.logcat({
|
||||
onlog: this.onLogcatContent.bind(this),
|
||||
onclose: this.onLogcatDisconnect.bind(this),
|
||||
}).then(x => {
|
||||
}).then(() => {
|
||||
// we successfully reconnected
|
||||
this._state = 'connected';
|
||||
this._prevlogs = null;
|
||||
this._initwait = null;
|
||||
resolve(this.content);
|
||||
}).fail(e => {
|
||||
}).fail((/*e*/) => {
|
||||
// reconnection failed - put the logs back and return the cached info
|
||||
this._logs = this._prevlogs._logs;
|
||||
this._htmllogs = this._prevlogs._htmllogs;
|
||||
@@ -81,8 +77,11 @@ class LogcatContent {
|
||||
});
|
||||
}
|
||||
sendClientMessage(msg) {
|
||||
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid);
|
||||
clients.forEach(client => client.send(msg+'\n')); // include a newline to try and persuade a buffer write
|
||||
LogcatContent._wss.clients.forEach(client => {
|
||||
if (client._logcatid === this._logcatid) {
|
||||
client.send(msg + '\n'); // include a newline to try and persuade a buffer write
|
||||
}
|
||||
})
|
||||
}
|
||||
sendDisconnectMsg() {
|
||||
this.sendClientMessage(':disconnect');
|
||||
@@ -115,7 +114,7 @@ class LogcatContent {
|
||||
}
|
||||
updateLogs() {
|
||||
// no point in formatting the data if there are no connected clients
|
||||
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid);
|
||||
var clients = [...LogcatContent._wss.clients].filter(client => client._logcatid === this._logcatid);
|
||||
if (clients.length) {
|
||||
var lines = '<div class="logblock">' + this._htmllogs.join('') + '</div>';
|
||||
clients.forEach(client => client.send(lines));
|
||||
@@ -161,7 +160,7 @@ class LogcatContent {
|
||||
this.renotify();
|
||||
}
|
||||
}
|
||||
onLogcatDisconnect(e) {
|
||||
onLogcatDisconnect(/*e*/) {
|
||||
if (this._state === 'disconnected') return;
|
||||
this._state = 'disconnected';
|
||||
this.sendDisconnectMsg();
|
||||
@@ -191,14 +190,19 @@ LogcatContent.initWebSocketServer = function () {
|
||||
port: wssport,
|
||||
retries: 0,
|
||||
tryCreateWSS() {
|
||||
this.wss = new WebSocketServer({ host: '127.0.0.1', port: this.port }, () => {
|
||||
const wsopts = {
|
||||
host: '127.0.0.1',
|
||||
port: this.port,
|
||||
clientTracking: true,
|
||||
};
|
||||
this.wss = new WebSocketServer(wsopts, () => {
|
||||
// success - save the info and resolve the deferred
|
||||
LogcatContent._wssport = this.port;
|
||||
LogcatContent._wssstartport = this.startport;
|
||||
LogcatContent._wss = this.wss;
|
||||
this.wss.on('connection', client => {
|
||||
this.wss.on('connection', (client, req) => {
|
||||
// the client uses the url path to signify which logcat data it wants
|
||||
client._logcatid = client.upgradeReq.url.match(/^\/?(.*)$/)[1];
|
||||
client._logcatid = req.url.match(/^\/?(.*)$/)[1];
|
||||
var lc = LogcatContent.byLogcatID[client._logcatid];
|
||||
if (lc) lc.onClientConnect(client);
|
||||
else client.close();
|
||||
@@ -215,7 +219,7 @@ LogcatContent.initWebSocketServer = function () {
|
||||
this.wss = null;
|
||||
LogcatContent._wssdone.resolveWith(LogcatContent, []);
|
||||
});
|
||||
this.wss.on('error', err => {
|
||||
this.wss.on('error', (/*err*/) => {
|
||||
if (!LogcatContent._wss) {
|
||||
// listen failed -try the next port
|
||||
this.retries++ , this.port++;
|
||||
@@ -245,7 +249,7 @@ function openLogcatWindow(vscode) {
|
||||
var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||
var adbargs = ['-P',''+adbport,'start-server'];
|
||||
try {
|
||||
var stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
|
||||
/*var stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
|
||||
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||
}
|
||||
})
|
||||
@@ -278,11 +282,26 @@ function openLogcatWindow(vscode) {
|
||||
.then(devices => {
|
||||
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
|
||||
devices.forEach(device => {
|
||||
if (vscode.window.createWebviewPanel) {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'androidlogcat', // Identifies the type of the webview. Used internally
|
||||
`logcat-${device.serial}`, // Title of the panel displayed to the user
|
||||
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
|
||||
{
|
||||
enableScripts: true,
|
||||
}
|
||||
);
|
||||
const logcat = new LogcatContent(device.serial);
|
||||
logcat.content.then(html => {
|
||||
panel.webview.html = html;
|
||||
});
|
||||
return;
|
||||
}
|
||||
var uri = AndroidContentProvider.getReadLogcatUri(device.serial);
|
||||
return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
|
||||
});
|
||||
})
|
||||
.fail(e => {
|
||||
.fail((/*e*/) => {
|
||||
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
|
||||
});
|
||||
}
|
||||
|
||||
11
src/state.js
Normal file
11
src/state.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const vscode = require('vscode');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
var adext = {};
|
||||
try {
|
||||
Object.assign(adext, JSON.parse(fs.readFileSync(path.join(path.dirname(__dirname),'package.json'),'utf8')));
|
||||
} catch (ex) { }
|
||||
|
||||
exports.adext = adext;
|
||||
608
src/util.js
608
src/util.js
@@ -1,43 +1,48 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
var nofn=function(){};
|
||||
var D=exports.D=console.log.bind(console);
|
||||
var E=exports.E=console.error.bind(console);
|
||||
var W=exports.W=console.warn.bind(console);
|
||||
var DD=nofn,cl=D,printf=D;
|
||||
var nofn = function () { };
|
||||
const messagePrintCallbacks = new Set();
|
||||
var D = exports.D = (...args) => (console.log(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
|
||||
var E = exports.E = (...args) => (console.error(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
|
||||
var W = exports.W = (...args) => (console.warn(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
|
||||
var DD = nofn, cl = D, printf = D;
|
||||
var print_jdwp_data = nofn;// _print_jdwp_data;
|
||||
var print_packet = nofn;//_print_packet;
|
||||
|
||||
Array.first = function(arr, fn, defaultvalue) {
|
||||
exports.onMessagePrint = function(cb) {
|
||||
messagePrintCallbacks.add(cb);
|
||||
}
|
||||
|
||||
Array.first = function (arr, fn, defaultvalue) {
|
||||
var idx = Array.indexOfFirst(arr, fn);
|
||||
return idx < 0 ? defaultvalue : arr[idx];
|
||||
}
|
||||
|
||||
Array.indexOfFirst = function(arr, fn) {
|
||||
Array.indexOfFirst = function (arr, fn) {
|
||||
if (!Array.isArray(arr)) return -1;
|
||||
for (var i=0; i < arr.length; i++)
|
||||
for (var i = 0; i < arr.length; i++)
|
||||
if (fn(arr[i], i, arr))
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
var isEmptyObject = exports.isEmptyObject = function(o) {
|
||||
return typeof(o)==='object' && !Object.keys(o).length;
|
||||
var isEmptyObject = exports.isEmptyObject = function (o) {
|
||||
return typeof (o) === 'object' && !Object.keys(o).length;
|
||||
}
|
||||
|
||||
var leftpad = exports.leftpad = function(char, len, s) {
|
||||
var leftpad = exports.leftpad = function (char, len, s) {
|
||||
while (s.length < len)
|
||||
s = char + s;
|
||||
return s;
|
||||
}
|
||||
|
||||
var intToHex = exports.intToHex = function(i, minlen) {
|
||||
var intToHex = exports.intToHex = function (i, minlen) {
|
||||
var s = i.toString(16);
|
||||
if (minlen) s = leftpad('0', minlen, s);
|
||||
return s;
|
||||
}
|
||||
|
||||
var intFromHex = exports.intFromHex = function(s, maxlen, defaultvalue) {
|
||||
var intFromHex = exports.intFromHex = function (s, maxlen, defaultvalue) {
|
||||
s = s.slice(0, maxlen);
|
||||
if (!/^[0-9a-fA-F]+$/.test(s)) return defaultvalue;
|
||||
return parseInt(s, 16);
|
||||
@@ -45,53 +50,53 @@ var intFromHex = exports.intFromHex = function(s, maxlen, defaultvalue) {
|
||||
|
||||
var fdcache = [];
|
||||
|
||||
var index_of_file_fdn = function(n) {
|
||||
var index_of_file_fdn = function (n) {
|
||||
if (n <= 0) return -1;
|
||||
for (var i=0; i < fdcache.length; i++) {
|
||||
for (var i = 0; i < fdcache.length; i++) {
|
||||
if (fdcache[i] && fdcache[i].n === n)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
var get_file_fd_from_fdn = function(n) {
|
||||
var get_file_fd_from_fdn = function (n) {
|
||||
var idx = index_of_file_fdn(n);
|
||||
if (idx < 0) return null;
|
||||
return fdcache[idx];
|
||||
}
|
||||
|
||||
var remove_fd_from_cache = function(fd) {
|
||||
var remove_fd_from_cache = function (fd) {
|
||||
if (!fd) return;
|
||||
var idx = index_of_file_fdn(fd.n);
|
||||
if (idx>=0) fdcache.splice(idx, 1);
|
||||
if (idx >= 0) fdcache.splice(idx, 1);
|
||||
}
|
||||
|
||||
// add an offset so we don't conflict with tcp socketIds
|
||||
var min_fd_num = 100000;
|
||||
var _new_fd_count = 0;
|
||||
var new_fd = this.new_fd = function(name, raw) {
|
||||
var new_fd = this.new_fd = function (name, raw) {
|
||||
var rwpipe = raw ? new Uint8Array(0) : [];
|
||||
var fd = {
|
||||
name: name,
|
||||
n: min_fd_num + (++_new_fd_count),
|
||||
raw: !!raw,
|
||||
readpipe:rwpipe,
|
||||
writepipe:rwpipe,
|
||||
reader:null,
|
||||
readerlen:0,
|
||||
kickingreader:false,
|
||||
total:{read:0,written:0},
|
||||
readpipe: rwpipe,
|
||||
writepipe: rwpipe,
|
||||
reader: null,
|
||||
readerlen: 0,
|
||||
kickingreader: false,
|
||||
total: { read: 0, written: 0 },
|
||||
duplex: null,
|
||||
closed:'',
|
||||
read:function(cb) {
|
||||
closed: '',
|
||||
read: function (cb) {
|
||||
if (this.raw)
|
||||
throw 'Cannot read from raw fd';
|
||||
if (this.reader && this.reader !== cb)
|
||||
throw 'multiple readers?';
|
||||
this.reader = cb;
|
||||
this._kickreader();
|
||||
this.reader = cb;
|
||||
this._kickreader();
|
||||
},
|
||||
write:function(data) {
|
||||
write: function (data) {
|
||||
if (this.closed) {
|
||||
D('Ignoring attempt to write to closed file: %o', this);
|
||||
return;
|
||||
@@ -100,23 +105,23 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
D('Ignoring attempt to write object to raw file: %o', this);
|
||||
return;
|
||||
}
|
||||
this.writepipe.push(data);
|
||||
if (this.duplex) {
|
||||
this.duplex._kickreader();
|
||||
}
|
||||
this.writepipe.push(data);
|
||||
if (this.duplex) {
|
||||
this.duplex._kickreader();
|
||||
}
|
||||
},
|
||||
|
||||
readbytes:function(len, cb) {
|
||||
readbytes: function (len, cb) {
|
||||
if (!this.raw)
|
||||
throw 'Cannot readbytes from non-raw fd';
|
||||
if (this.reader)
|
||||
throw 'multiple readers?';
|
||||
this.reader = cb;
|
||||
this.reader = cb;
|
||||
this.readerlen = len;
|
||||
this._kickreader();
|
||||
this._kickreader();
|
||||
},
|
||||
|
||||
writebytes:function(buffer) {
|
||||
writebytes: function (buffer) {
|
||||
if (this.closed) {
|
||||
D('Ignoring attempt to write to closed file: %o', this);
|
||||
return;
|
||||
@@ -135,35 +140,35 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
newbuf.set(this.writepipe);
|
||||
newbuf.set(buffer, this.writepipe.byteLength);
|
||||
this.writepipe = newbuf;
|
||||
if (this.duplex)
|
||||
if (this.duplex)
|
||||
this.duplex.readpipe = newbuf;
|
||||
else
|
||||
this.readpipe = newbuf;
|
||||
D('new buffer size: %d (fd:%d)',this.writepipe.byteLength, this.n);
|
||||
D('new buffer size: %d (fd:%d)', this.writepipe.byteLength, this.n);
|
||||
this._kickreaders();
|
||||
},
|
||||
|
||||
cancelread:function(flushfirst) {
|
||||
cancelread: function (flushfirst) {
|
||||
if (flushfirst)
|
||||
this.flush();
|
||||
this.reader = null;
|
||||
this.readerlen = 0;
|
||||
},
|
||||
|
||||
write_eof:function() {
|
||||
write_eof: function () {
|
||||
this.flush();
|
||||
// eof is only relevant for read-until-close readers
|
||||
if (this.raw && this.reader && this.readerlen === -1) {
|
||||
this.reader({err:'eof'});
|
||||
this.reader({ err: 'eof' });
|
||||
}
|
||||
},
|
||||
|
||||
flush:function() {
|
||||
flush: function () {
|
||||
this._doread();
|
||||
},
|
||||
|
||||
close:function() {
|
||||
if (this.closed)
|
||||
close: function () {
|
||||
if (this.closed)
|
||||
return;
|
||||
console.trace('Closing file %d: %o', this.n, this);
|
||||
this.closed = 'closed';
|
||||
@@ -175,24 +180,24 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
remove_fd_from_cache(this);
|
||||
},
|
||||
|
||||
_kickreaders:function() {
|
||||
_kickreaders: function () {
|
||||
if (this.duplex)
|
||||
this.duplex._kickreader();
|
||||
else
|
||||
this._kickreader();
|
||||
},
|
||||
|
||||
_kickreader:function() {
|
||||
if (!this.reader) return;
|
||||
if (this.kickingreader) return;
|
||||
var t = this;
|
||||
t.kickingreader = setTimeout(function() {
|
||||
t.kickingreader = false;
|
||||
t._doreadcheckclose();
|
||||
}, 0);
|
||||
_kickreader: function () {
|
||||
if (!this.reader) return;
|
||||
if (this.kickingreader) return;
|
||||
var t = this;
|
||||
t.kickingreader = setTimeout(function () {
|
||||
t.kickingreader = false;
|
||||
t._doreadcheckclose();
|
||||
}, 0);
|
||||
},
|
||||
|
||||
_doreadcheckclose:function() {
|
||||
|
||||
_doreadcheckclose: function () {
|
||||
var cs = this.closed;
|
||||
this._doread();
|
||||
if (cs) {
|
||||
@@ -203,13 +208,13 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
this.readerlen = 0;
|
||||
if (rucreader && rucreadercb) {
|
||||
// terminate the read-until-close reader
|
||||
D('terminating ruc reader. fd: %o',this);
|
||||
rucreadercb({err:'File closed'});
|
||||
D('terminating ruc reader. fd: %o', this);
|
||||
rucreadercb({ err: 'File closed' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_doread:function() {
|
||||
_doread: function () {
|
||||
if (this.raw) {
|
||||
if (!this.reader) return;
|
||||
if (this.readerlen > this.readpipe.byteLength) return;
|
||||
@@ -218,7 +223,7 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
this.reader = null, this.readerlen = 0;
|
||||
var data;
|
||||
if (len) {
|
||||
var readlen = len>0?len:this.readpipe.byteLength;
|
||||
var readlen = len > 0 ? len : this.readpipe.byteLength;
|
||||
data = this.readpipe.subarray(0, readlen);
|
||||
this.readpipe = this.readpipe.subarray(readlen);
|
||||
if (this.duplex)
|
||||
@@ -230,17 +235,17 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
data = new Uint8Array(0);
|
||||
}
|
||||
|
||||
data.asString = function() {
|
||||
data.asString = function () {
|
||||
return uint8ArrayToString(this);
|
||||
};
|
||||
data.intFromHex = function(len) {
|
||||
len = len||this.byteLength;
|
||||
var x = this.asString().slice(0,len);
|
||||
data.intFromHex = function (len) {
|
||||
len = len || this.byteLength;
|
||||
var x = this.asString().slice(0, len);
|
||||
if (!/^[0-9a-fA-F]+/.test(x)) return -1;
|
||||
return parseInt(x, 16);
|
||||
}
|
||||
cb(null, data);
|
||||
|
||||
|
||||
if (len < 0) {
|
||||
// reset the reader
|
||||
this.readbytes(len, cb);
|
||||
@@ -259,103 +264,103 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
return fd;
|
||||
}
|
||||
|
||||
var intToCharString = function(n) {
|
||||
var intToCharString = function (n) {
|
||||
return String.fromCharCode(
|
||||
(n>>0)&255,
|
||||
(n>>8)&255,
|
||||
(n>>16)&255,
|
||||
(n>>24)&255
|
||||
(n >> 0) & 255,
|
||||
(n >> 8) & 255,
|
||||
(n >> 16) & 255,
|
||||
(n >> 24) & 255
|
||||
);
|
||||
}
|
||||
|
||||
var stringToUint8Array = function(s) {
|
||||
var stringToUint8Array = function (s) {
|
||||
var x = new Uint8Array(s.length);
|
||||
for (var i=0; i < s.length; i++)
|
||||
for (var i = 0; i < s.length; i++)
|
||||
x[i] = s.charCodeAt(i);
|
||||
return x;
|
||||
}
|
||||
|
||||
var uint8ArrayToString = function(a) {
|
||||
var uint8ArrayToString = function (a) {
|
||||
var s = new Array(a.byteLength);
|
||||
for (var i=0; i < a.byteLength; i++)
|
||||
for (var i = 0; i < a.byteLength; i++)
|
||||
s[i] = a[i];
|
||||
return String.fromCharCode.apply(String, s);
|
||||
}
|
||||
|
||||
// asynchronous array iterater
|
||||
var iterate = function(arr, o) {
|
||||
var isrange = typeof(arr)==='number';
|
||||
if (isrange)
|
||||
arr = { length: arr<0?0:arr };
|
||||
var iterate = function (arr, o) {
|
||||
var isrange = typeof (arr) === 'number';
|
||||
if (isrange)
|
||||
arr = { length: arr < 0 ? 0 : arr };
|
||||
var x = {
|
||||
value:arr,
|
||||
isrange:isrange,
|
||||
first:o.first||nofn,
|
||||
each:o.each||(function() { this.next(); }),
|
||||
last:o.last||nofn,
|
||||
success:o.success||nofn,
|
||||
error:o.error||nofn,
|
||||
complete:o.complete||nofn,
|
||||
_idx:0,
|
||||
_donefirst:false,
|
||||
_donelast:false,
|
||||
abort:function(err) {
|
||||
value: arr,
|
||||
isrange: isrange,
|
||||
first: o.first || nofn,
|
||||
each: o.each || (function () { this.next(); }),
|
||||
last: o.last || nofn,
|
||||
success: o.success || nofn,
|
||||
error: o.error || nofn,
|
||||
complete: o.complete || nofn,
|
||||
_idx: 0,
|
||||
_donefirst: false,
|
||||
_donelast: false,
|
||||
abort: function (err) {
|
||||
this.error(err);
|
||||
this.complete();
|
||||
return;
|
||||
},
|
||||
finish:function(res) {
|
||||
finish: function (res) {
|
||||
// finish early
|
||||
if (typeof(res)!=='undefined') this.result = res;
|
||||
this.success(res||this.result);
|
||||
if (typeof (res) !== 'undefined') this.result = res;
|
||||
this.success(res || this.result);
|
||||
this.complete();
|
||||
return;
|
||||
},
|
||||
iteratefirst:function() {
|
||||
iteratefirst: function () {
|
||||
if (!this.value.length) {
|
||||
this.finish();
|
||||
return;
|
||||
}
|
||||
this.first(this.value[this._idx],this._idx,this);
|
||||
this.each(this.value[this._idx],this._idx,this);
|
||||
this.first(this.value[this._idx], this._idx, this);
|
||||
this.each(this.value[this._idx], this._idx, this);
|
||||
},
|
||||
iteratenext:function() {
|
||||
iteratenext: function () {
|
||||
if (++this._idx >= this.value.length) {
|
||||
this.last(this.value[this._idx],this._idx,this);
|
||||
this.last(this.value[this._idx], this._idx, this);
|
||||
this.finish();
|
||||
return;
|
||||
}
|
||||
this.each(this.value[this._idx],this._idx,this);
|
||||
this.each(this.value[this._idx], this._idx, this);
|
||||
},
|
||||
next:function() {
|
||||
next: function () {
|
||||
var t = this;
|
||||
setTimeout(function() {
|
||||
t.iteratenext();
|
||||
},0);
|
||||
setTimeout(function () {
|
||||
t.iteratenext();
|
||||
}, 0);
|
||||
},
|
||||
nextorabort:function(err) {
|
||||
if (err) this.abort(err);
|
||||
else this.next();
|
||||
nextorabort: function (err) {
|
||||
if (err) this.abort(err);
|
||||
else this.next();
|
||||
},
|
||||
};
|
||||
setTimeout(function() { x.iteratefirst(); }, 0);
|
||||
setTimeout(function () { x.iteratefirst(); }, 0);
|
||||
return x;
|
||||
};
|
||||
|
||||
var iterate_repeat = function(arr, count, o, j) {
|
||||
var iterate_repeat = function (arr, count, o, j) {
|
||||
iterate(arr, {
|
||||
each: function(value, i, it) {
|
||||
o.each(value, i, j||0, it);
|
||||
each: function (value, i, it) {
|
||||
o.each(value, i, j || 0, it);
|
||||
},
|
||||
success: function() {
|
||||
success: function () {
|
||||
if (!--count) {
|
||||
o.success && o.success();
|
||||
o.complete && o.complete();
|
||||
return;
|
||||
}
|
||||
iterate_repeat(arr, count, o, (j||0)+1);
|
||||
iterate_repeat(arr, count, o, (j || 0) + 1);
|
||||
},
|
||||
error:function(err) {
|
||||
error: function (err) {
|
||||
o.error && o.error();
|
||||
o.complete && o.complete();
|
||||
}
|
||||
@@ -367,13 +372,13 @@ var iterate_repeat = function(arr, count, o, j) {
|
||||
* @param {ArrayBuffer} buffer The array buffer to convert.
|
||||
* @return {string} The textual representation of the array.
|
||||
*/
|
||||
var arrayBufferToString = exports.arrayBufferToString = function(buffer) {
|
||||
var array = new Uint8Array(buffer);
|
||||
var str = '';
|
||||
for (var i = 0; i < array.length; ++i) {
|
||||
str += String.fromCharCode(array[i]);
|
||||
}
|
||||
return str;
|
||||
var arrayBufferToString = exports.arrayBufferToString = function (buffer) {
|
||||
var array = new Uint8Array(buffer);
|
||||
var str = '';
|
||||
for (var i = 0; i < array.length; ++i) {
|
||||
str += String.fromCharCode(array[i]);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -381,59 +386,59 @@ var arrayBufferToString = exports.arrayBufferToString = function(buffer) {
|
||||
* @param {array} UTF-8 array
|
||||
* @return {string} UTF-8 string
|
||||
*/
|
||||
var ary2utf8 = (function() {
|
||||
var ary2utf8 = (function () {
|
||||
|
||||
var patterns = [
|
||||
{pattern: '0xxxxxxx', bytes: 1},
|
||||
{pattern: '110xxxxx', bytes: 2},
|
||||
{pattern: '1110xxxx', bytes: 3},
|
||||
{pattern: '11110xxx', bytes: 4},
|
||||
{pattern: '111110xx', bytes: 5},
|
||||
{pattern: '1111110x', bytes: 6}
|
||||
];
|
||||
patterns.forEach(function(item) {
|
||||
item.header = item.pattern.replace(/[^10]/g, '');
|
||||
item.pattern01 = item.pattern.replace(/[^10]/g, '0');
|
||||
item.pattern01 = parseInt(item.pattern01, 2);
|
||||
item.mask_length = item.header.length;
|
||||
item.data_length = 8 - item.header.length;
|
||||
var mask = '';
|
||||
for (var i = 0, len = item.mask_length; i < len; i++) {
|
||||
mask += '1';
|
||||
}
|
||||
for (var i = 0, len = item.data_length; i < len; i++) {
|
||||
mask += '0';
|
||||
}
|
||||
item.mask = mask;
|
||||
item.mask = parseInt(item.mask, 2);
|
||||
});
|
||||
var patterns = [
|
||||
{ pattern: '0xxxxxxx', bytes: 1 },
|
||||
{ pattern: '110xxxxx', bytes: 2 },
|
||||
{ pattern: '1110xxxx', bytes: 3 },
|
||||
{ pattern: '11110xxx', bytes: 4 },
|
||||
{ pattern: '111110xx', bytes: 5 },
|
||||
{ pattern: '1111110x', bytes: 6 }
|
||||
];
|
||||
patterns.forEach(function (item) {
|
||||
item.header = item.pattern.replace(/[^10]/g, '');
|
||||
item.pattern01 = item.pattern.replace(/[^10]/g, '0');
|
||||
item.pattern01 = parseInt(item.pattern01, 2);
|
||||
item.mask_length = item.header.length;
|
||||
item.data_length = 8 - item.header.length;
|
||||
var mask = '';
|
||||
for (var i = 0, len = item.mask_length; i < len; i++) {
|
||||
mask += '1';
|
||||
}
|
||||
for (var i = 0, len = item.data_length; i < len; i++) {
|
||||
mask += '0';
|
||||
}
|
||||
item.mask = mask;
|
||||
item.mask = parseInt(item.mask, 2);
|
||||
});
|
||||
|
||||
return function(ary) {
|
||||
var codes = [];
|
||||
var cur = 0;
|
||||
while(cur < ary.length) {
|
||||
var first = ary[cur];
|
||||
var pattern = null;
|
||||
for (var i = 0, len = patterns.length; i < len; i++) {
|
||||
if ((first & patterns[i].mask) == patterns[i].pattern01) {
|
||||
pattern = patterns[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pattern == null) {
|
||||
throw 'utf-8 decode error';
|
||||
}
|
||||
var rest = ary.slice(cur + 1, cur + pattern.bytes);
|
||||
cur += pattern.bytes;
|
||||
var code = '';
|
||||
code += ('00000000' + (first & (255 ^ pattern.mask)).toString(2)).slice(-pattern.data_length);
|
||||
for (var i = 0, len = rest.length; i < len; i++) {
|
||||
code += ('00000000' + (rest[i] & parseInt('111111', 2)).toString(2)).slice(-6);
|
||||
}
|
||||
codes.push(parseInt(code, 2));
|
||||
}
|
||||
return String.fromCharCode.apply(null, codes);
|
||||
};
|
||||
return function (ary) {
|
||||
var codes = [];
|
||||
var cur = 0;
|
||||
while (cur < ary.length) {
|
||||
var first = ary[cur];
|
||||
var pattern = null;
|
||||
for (var i = 0, len = patterns.length; i < len; i++) {
|
||||
if ((first & patterns[i].mask) == patterns[i].pattern01) {
|
||||
pattern = patterns[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pattern == null) {
|
||||
throw 'utf-8 decode error';
|
||||
}
|
||||
var rest = ary.slice(cur + 1, cur + pattern.bytes);
|
||||
cur += pattern.bytes;
|
||||
var code = '';
|
||||
code += ('00000000' + (first & (255 ^ pattern.mask)).toString(2)).slice(-pattern.data_length);
|
||||
for (var i = 0, len = rest.length; i < len; i++) {
|
||||
code += ('00000000' + (rest[i] & parseInt('111111', 2)).toString(2)).slice(-6);
|
||||
}
|
||||
codes.push(parseInt(code, 2));
|
||||
}
|
||||
return String.fromCharCode.apply(null, codes);
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -442,52 +447,52 @@ var ary2utf8 = (function() {
|
||||
* @param {string} UTF-8 string
|
||||
* @return {array} UTF-8 array
|
||||
*/
|
||||
var utf82ary = (function() {
|
||||
var utf82ary = (function () {
|
||||
|
||||
var patterns = [
|
||||
{pattern: '0xxxxxxx', bytes: 1},
|
||||
{pattern: '110xxxxx', bytes: 2},
|
||||
{pattern: '1110xxxx', bytes: 3},
|
||||
{pattern: '11110xxx', bytes: 4},
|
||||
{pattern: '111110xx', bytes: 5},
|
||||
{pattern: '1111110x', bytes: 6}
|
||||
];
|
||||
patterns.forEach(function(item) {
|
||||
item.header = item.pattern.replace(/[^10]/g, '');
|
||||
item.mask_length = item.header.length;
|
||||
item.data_length = 8 - item.header.length;
|
||||
item.max_bit_length = (item.bytes - 1) * 6 + item.data_length;
|
||||
});
|
||||
var patterns = [
|
||||
{ pattern: '0xxxxxxx', bytes: 1 },
|
||||
{ pattern: '110xxxxx', bytes: 2 },
|
||||
{ pattern: '1110xxxx', bytes: 3 },
|
||||
{ pattern: '11110xxx', bytes: 4 },
|
||||
{ pattern: '111110xx', bytes: 5 },
|
||||
{ pattern: '1111110x', bytes: 6 }
|
||||
];
|
||||
patterns.forEach(function (item) {
|
||||
item.header = item.pattern.replace(/[^10]/g, '');
|
||||
item.mask_length = item.header.length;
|
||||
item.data_length = 8 - item.header.length;
|
||||
item.max_bit_length = (item.bytes - 1) * 6 + item.data_length;
|
||||
});
|
||||
|
||||
var code2utf8array = function(code) {
|
||||
var pattern = null;
|
||||
var code01 = code.toString(2);
|
||||
for (var i = 0, len = patterns.length; i < len; i++) {
|
||||
if (code01.length <= patterns[i].max_bit_length) {
|
||||
pattern = patterns[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pattern == null) {
|
||||
throw 'utf-8 encode error';
|
||||
}
|
||||
var ary = [];
|
||||
for (var i = 0, len = pattern.bytes - 1; i < len; i++) {
|
||||
ary.unshift(parseInt('10' + ('000000' + code01.slice(-6)).slice(-6), 2));
|
||||
code01 = code01.slice(0, -6);
|
||||
}
|
||||
ary.unshift(parseInt(pattern.header + ('00000000' + code01).slice(-pattern.data_length), 2));
|
||||
return ary;
|
||||
};
|
||||
var code2utf8array = function (code) {
|
||||
var pattern = null;
|
||||
var code01 = code.toString(2);
|
||||
for (var i = 0, len = patterns.length; i < len; i++) {
|
||||
if (code01.length <= patterns[i].max_bit_length) {
|
||||
pattern = patterns[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pattern == null) {
|
||||
throw 'utf-8 encode error';
|
||||
}
|
||||
var ary = [];
|
||||
for (var i = 0, len = pattern.bytes - 1; i < len; i++) {
|
||||
ary.unshift(parseInt('10' + ('000000' + code01.slice(-6)).slice(-6), 2));
|
||||
code01 = code01.slice(0, -6);
|
||||
}
|
||||
ary.unshift(parseInt(pattern.header + ('00000000' + code01).slice(-pattern.data_length), 2));
|
||||
return ary;
|
||||
};
|
||||
|
||||
return function(str) {
|
||||
var codes = [];
|
||||
for (var i = 0, len = str.length; i < len; i++) {
|
||||
var code = str.charCodeAt(i);
|
||||
Array.prototype.push.apply(codes, code2utf8array(code));
|
||||
}
|
||||
return codes;
|
||||
};
|
||||
return function (str) {
|
||||
var codes = [];
|
||||
for (var i = 0, len = str.length; i < len; i++) {
|
||||
var code = str.charCodeAt(i);
|
||||
Array.prototype.push.apply(codes, code2utf8array(code));
|
||||
}
|
||||
return codes;
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -496,89 +501,88 @@ var utf82ary = (function() {
|
||||
* @param {string} string The string to convert.
|
||||
* @return {ArrayBuffer} An array buffer whose bytes correspond to the string.
|
||||
*/
|
||||
var stringToArrayBuffer = exports.stringToArrayBuffer = function(string) {
|
||||
var buffer = new ArrayBuffer(string.length);
|
||||
var bufferView = new Uint8Array(buffer);
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
bufferView[i] = string.charCodeAt(i);
|
||||
}
|
||||
return buffer;
|
||||
var stringToArrayBuffer = exports.stringToArrayBuffer = function (string) {
|
||||
var buffer = new ArrayBuffer(string.length);
|
||||
var bufferView = new Uint8Array(buffer);
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
bufferView[i] = string.charCodeAt(i);
|
||||
}
|
||||
return buffer;
|
||||
};
|
||||
|
||||
var str2ab = exports.str2ab = stringToArrayBuffer;
|
||||
var ab2str = exports.ab2str = arrayBufferToString;
|
||||
var str2u8arr = exports.str2u8arr = function(s) {
|
||||
var str2u8arr = exports.str2u8arr = function (s) {
|
||||
return new Uint8Array(str2ab(s));
|
||||
}
|
||||
|
||||
exports.getutf8bytes = function(str) {
|
||||
var utf8 = [];
|
||||
for (var i=0; i < str.length; i++) {
|
||||
var charcode = str.charCodeAt(i);
|
||||
if (charcode < 0x80) utf8.push(charcode);
|
||||
else if (charcode < 0x800) {
|
||||
utf8.push(0xc0 | (charcode >> 6),
|
||||
0x80 | (charcode & 0x3f));
|
||||
}
|
||||
else if (charcode < 0xd800 || charcode >= 0xe000) {
|
||||
utf8.push(0xe0 | (charcode >> 12),
|
||||
0x80 | ((charcode>>6) & 0x3f),
|
||||
0x80 | (charcode & 0x3f));
|
||||
}
|
||||
// surrogate pair
|
||||
else {
|
||||
i++;
|
||||
// UTF-16 encodes 0x10000-0x10FFFF by
|
||||
// subtracting 0x10000 and splitting the
|
||||
// 20 bits of 0x0-0xFFFFF into two halves
|
||||
charcode = 0x10000 + (((charcode & 0x3ff)<<10)
|
||||
| (str.charCodeAt(i) & 0x3ff));
|
||||
utf8.push(0xf0 | (charcode >>18),
|
||||
0x80 | ((charcode>>12) & 0x3f),
|
||||
0x80 | ((charcode>>6) & 0x3f),
|
||||
0x80 | (charcode & 0x3f));
|
||||
}
|
||||
}
|
||||
return utf8;
|
||||
}
|
||||
|
||||
exports.fromutf8bytes = function(array) {
|
||||
var out, i, len, c;
|
||||
var char2, char3;
|
||||
|
||||
out = "";
|
||||
len = array.length;
|
||||
i = 0;
|
||||
while(i < len) {
|
||||
c = array[i++];
|
||||
switch(c >> 4)
|
||||
{
|
||||
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
|
||||
// 0xxxxxxx
|
||||
out += String.fromCharCode(c);
|
||||
break;
|
||||
case 12: case 13:
|
||||
// 110x xxxx 10xx xxxx
|
||||
char2 = array[i++];
|
||||
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
|
||||
break;
|
||||
case 14:
|
||||
// 1110 xxxx 10xx xxxx 10xx xxxx
|
||||
char2 = array[i++];
|
||||
char3 = array[i++];
|
||||
out += String.fromCharCode(((c & 0x0F) << 12) |
|
||||
((char2 & 0x3F) << 6) |
|
||||
((char3 & 0x3F) << 0));
|
||||
break;
|
||||
exports.getutf8bytes = function (str) {
|
||||
var utf8 = [];
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
var charcode = str.charCodeAt(i);
|
||||
if (charcode < 0x80) utf8.push(charcode);
|
||||
else if (charcode < 0x800) {
|
||||
utf8.push(0xc0 | (charcode >> 6),
|
||||
0x80 | (charcode & 0x3f));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
else if (charcode < 0xd800 || charcode >= 0xe000) {
|
||||
utf8.push(0xe0 | (charcode >> 12),
|
||||
0x80 | ((charcode >> 6) & 0x3f),
|
||||
0x80 | (charcode & 0x3f));
|
||||
}
|
||||
// surrogate pair
|
||||
else {
|
||||
i++;
|
||||
// UTF-16 encodes 0x10000-0x10FFFF by
|
||||
// subtracting 0x10000 and splitting the
|
||||
// 20 bits of 0x0-0xFFFFF into two halves
|
||||
charcode = 0x10000 + (((charcode & 0x3ff) << 10)
|
||||
| (str.charCodeAt(i) & 0x3ff));
|
||||
utf8.push(0xf0 | (charcode >> 18),
|
||||
0x80 | ((charcode >> 12) & 0x3f),
|
||||
0x80 | ((charcode >> 6) & 0x3f),
|
||||
0x80 | (charcode & 0x3f));
|
||||
}
|
||||
}
|
||||
return utf8;
|
||||
}
|
||||
|
||||
exports.arraybuffer_concat = function() {
|
||||
var bufs=[], total=0;
|
||||
for (var i=0; i < arguments.length; i++) {
|
||||
exports.fromutf8bytes = function (array) {
|
||||
var out, i, len, c;
|
||||
var char2, char3;
|
||||
|
||||
out = "";
|
||||
len = array.length;
|
||||
i = 0;
|
||||
while (i < len) {
|
||||
c = array[i++];
|
||||
switch (c >> 4) {
|
||||
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
|
||||
// 0xxxxxxx
|
||||
out += String.fromCharCode(c);
|
||||
break;
|
||||
case 12: case 13:
|
||||
// 110x xxxx 10xx xxxx
|
||||
char2 = array[i++];
|
||||
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
|
||||
break;
|
||||
case 14:
|
||||
// 1110 xxxx 10xx xxxx 10xx xxxx
|
||||
char2 = array[i++];
|
||||
char3 = array[i++];
|
||||
out += String.fromCharCode(((c & 0x0F) << 12) |
|
||||
((char2 & 0x3F) << 6) |
|
||||
((char3 & 0x3F) << 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
exports.arraybuffer_concat = function () {
|
||||
var bufs = [], total = 0;
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var a = arguments[i];
|
||||
if (!a || !a.byteLength) continue;
|
||||
bufs.push(a);
|
||||
@@ -589,16 +593,16 @@ exports.arraybuffer_concat = function() {
|
||||
case 1: return new Uint8Array(bufs[0]);
|
||||
}
|
||||
var res = new Uint8Array(total);
|
||||
for (var i=0, j=0; i < bufs.length; i++) {
|
||||
for (var i = 0, j = 0; i < bufs.length; i++) {
|
||||
res.set(bufs[i], j);
|
||||
j += bufs[i].byteLength;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
exports.remove_from_list = function(arr, item, searchfn) {
|
||||
if (!searchfn) searchfn = function(a,b) { return a===b; };
|
||||
for (var i=0; i < arr.length; i++) {
|
||||
exports.remove_from_list = function (arr, item, searchfn) {
|
||||
if (!searchfn) searchfn = function (a, b) { return a === b; };
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
var found = searchfn(arr[i], item);
|
||||
if (found) {
|
||||
return {
|
||||
@@ -610,22 +614,22 @@ exports.remove_from_list = function(arr, item, searchfn) {
|
||||
D('Object %o not removed from list %o', item, arr);
|
||||
}
|
||||
|
||||
exports.dumparr = function(arr, offset, count) {
|
||||
offset=offset||0;
|
||||
count = count||(count===0?0:arr.length);
|
||||
if (count > arr.length-offset)
|
||||
count = arr.length-offset;
|
||||
exports.dumparr = function (arr, offset, count) {
|
||||
offset = offset || 0;
|
||||
count = count || (count === 0 ? 0 : arr.length);
|
||||
if (count > arr.length - offset)
|
||||
count = arr.length - offset;
|
||||
var s = '';
|
||||
while (count--) {
|
||||
s += ' '+('00'+arr[offset++].toString(16)).slice(-2);
|
||||
s += ' ' + ('00' + arr[offset++].toString(16)).slice(-2);
|
||||
}
|
||||
return s.slice(1);
|
||||
}
|
||||
|
||||
exports.btoa = function(arr) {
|
||||
return new Buffer(arr,'binary').toString('base64');
|
||||
exports.btoa = function (arr) {
|
||||
return Buffer.from(arr, 'binary').toString('base64');
|
||||
}
|
||||
|
||||
exports.atob = function(base64) {
|
||||
return new Buffer(base64, 'base64').toString('binary');
|
||||
exports.atob = function (base64) {
|
||||
return Buffer.from(base64, 'base64').toString('binary');
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ class AndroidVariables {
|
||||
value: x.varinfo.objvar.value,
|
||||
valid:true,
|
||||
});
|
||||
// create the fully qualified names to use for evaluation
|
||||
fields.forEach(f => f.fqname = `${x.varinfo.objvar.fqname || x.varinfo.objvar.name}.${f.name}`);
|
||||
x.varinfo.cached = fields;
|
||||
return this._local_to_variable(fields);
|
||||
});
|
||||
@@ -100,9 +102,10 @@ class AndroidVariables {
|
||||
return $.Deferred().resolve(variables);
|
||||
}
|
||||
// get the elements for the specified range
|
||||
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count)
|
||||
.then((elements) => {
|
||||
varinfo.cached = elements;
|
||||
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count, {varinfo})
|
||||
.then((elements, x) => {
|
||||
elements.forEach(el => el.fqname = `${x.varinfo.arrvar.fqname || x.varinfo.arrvar.name}[${el.name}]`);
|
||||
x.varinfo.cached = elements;
|
||||
return this._local_to_variable(elements);
|
||||
});
|
||||
}
|
||||
@@ -158,7 +161,7 @@ class AndroidVariables {
|
||||
*/
|
||||
_local_to_variable(v) {
|
||||
if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._local_to_variable(v));
|
||||
var varref = 0, objvalue, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename;
|
||||
var varref = 0, objvalue, evaluateName = v.fqname || v.name, formats = {}, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename;
|
||||
switch(true) {
|
||||
case v.hasnullvalue && JTYPES.isReference(v.type):
|
||||
// null object or array type
|
||||
@@ -188,7 +191,7 @@ class AndroidVariables {
|
||||
varref = this._getObjectIdReference(v.type, v.value);
|
||||
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] };
|
||||
}
|
||||
objvalue = v.type.typename.replace(/]$/, v.arraylen+']'); // insert len as the final array bound
|
||||
objvalue = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
|
||||
break;
|
||||
case JTYPES.isObject(v.type):
|
||||
// non-null object instance - add another variable reference so the user can expand
|
||||
@@ -207,10 +210,27 @@ class AndroidVariables {
|
||||
case v.type.signature === 'J':
|
||||
// because JS cannot handle 64bit ints, we need a bit of extra work
|
||||
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
|
||||
objvalue = NumberBaseConverter.hexToDec(v64hex, true);
|
||||
objvalue = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
|
||||
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
|
||||
formats.oct = formats.bin = '';
|
||||
// 24 bit chunks...
|
||||
for (var s=v64hex,uint; s; s = s.slice(0,-6)) {
|
||||
uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits
|
||||
formats.oct = uint.toString(8) + formats.oct;
|
||||
formats.bin = uint.toString(2) + formats.bin;
|
||||
}
|
||||
formats.oct = '0c' + formats.oct.replace(/^0+/, '0');
|
||||
formats.bin = '0b' + formats.bin.replace(/^0+/, '0');
|
||||
break;
|
||||
case /^[BIS]$/.test(v.type.signature):
|
||||
objvalue = formats.dec = v.value.toString();
|
||||
var uint = (v.value >>> 0);
|
||||
formats.hex = '0x' + uint.toString(16);
|
||||
formats.oct = '0c' + uint.toString(8);
|
||||
formats.bin = '0b' + uint.toString(2);
|
||||
break;
|
||||
default:
|
||||
// other primitives: int, boolean, etc
|
||||
// other primitives: boolean, etc
|
||||
objvalue = v.value.toString();
|
||||
break;
|
||||
}
|
||||
@@ -223,6 +243,7 @@ class AndroidVariables {
|
||||
name: v.name,
|
||||
type: typename,
|
||||
value: objvalue,
|
||||
evaluateName,
|
||||
variablesReference: varref,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user