From 2727dd1b0c9d5c1bfdc4a5df9896d1ecfc19c92c Mon Sep 17 00:00:00 2001 From: adelphes Date: Tue, 31 Jan 2017 13:40:54 +0000 Subject: [PATCH] Refactored setBreakPointsRequest Breakpoints are now updated using an async queue Allow source locations inside ANDROID_HOME Fixes #14 --- src/debugMain.js | 89 +++++++++++++++++++++++++++++++++++------------- src/debugger.js | 54 ++++++++++++++--------------- 2 files changed, 93 insertions(+), 50 deletions(-) diff --git a/src/debugMain.js b/src/debugMain.js index 8f94161..760f3ae 100644 --- a/src/debugMain.js +++ b/src/debugMain.js @@ -54,6 +54,12 @@ function ensure_path_end_slash(p) { return p + (/[\\/]$/.test(p) ? '' : path.sep); } +function is_subpath_of(fpn, subpath) { + if (!subpath || !fpn) return false; + subpath = ensure_path_end_slash(''+subpath); + return fpn.slice(0,subpath.length) === subpath; +} + function decode_char(c) { switch(true) { case /^\\[^u]$/.test(c): @@ -520,11 +526,16 @@ class AndroidDebugSession extends DebugSession { if ((pkginfo = this.src_packages.packages[pkg]).package_path === srcfolder) break; pkginfo = null; } - if (!pkginfo || !/\.java$/.test(srcfpn)) { + // 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)) { // 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 outside of the project source tree')) + breakpoints: args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid')) }; this.sendResponse(response); return; @@ -532,7 +543,7 @@ class AndroidDebugSession extends DebugSession { // our debugger requires a relative fpn beginning with / , rooted at the java source base folder // - it should look like: /some/package/name/abc.java - var relative_fpn = srcfpn.slice(pkginfo.srcroot.length); + var relative_fpn = srcfpn.slice(pkginfo.srcroot.match(/^(.*?)[\\/]?$/)[1].length); // delete any existing breakpoints not in the list var src_line_nums = args.breakpoints.map(bp => bp.line); @@ -543,7 +554,14 @@ class AndroidDebugSession extends DebugSession { }); // return the list of new and existing breakpoints - var breakpoints = args.breakpoints.map((src_bp,idx) => { + // - setting a debugger bp is now asynchronous, so we do this as an orderly queue + const _setup_breakpoints = (o, idx, javabp_arr) => { + javabp_arr = javabp_arr || []; + var src_bp = o.args.breakpoints[idx|=0]; + if (!src_bp) { + // done + return $.Deferred().resolveWith(this, [javabp_arr]); + } var dbgline = this.convertClientLineToDebugger(src_bp.line); var options = {}; if (src_bp.hitCondition) { @@ -554,26 +572,51 @@ class AndroidDebugSession extends DebugSession { if (!m || hitcount < 0 || hitcount > 0x7fffffff) return unverified_breakpoint(src_bp, 'The breakpoint is configured with an invalid hit count value'); options.hitcount = hitcount; } - var javabp = this.dbgr.setbreakpoint(relative_fpn, dbgline, options); - if (!javabp.vsbp) { - // state is one of: set,notloaded,enabled,removed - var verified = !!javabp.state.match(/set|enabled/); - const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(dbgline)); - // the breakpoint *must* have an id field or it won't update properly - bp.id = ++this._breakpointId; - if (javabp.state === 'notloaded') - bp.message = 'The runtime hasn\'t loaded this code location'; - javabp.vsbp = bp; - } - javabp.vsbp.order = idx; - return javabp.vsbp; - }); + return this.dbgr.setbreakpoint(o.relative_fpn, dbgline, options) + .then(javabp => { + if (!javabp.vsbp) { + // state is one of: set,notloaded,enabled,removed + var verified = !!javabp.state.match(/set|enabled/); + const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(dbgline)); + // the breakpoint *must* have an id field or it won't update properly + bp.id = ++this._breakpointId; + if (javabp.state === 'notloaded') + bp.message = 'The runtime hasn\'t loaded this code location'; + javabp.vsbp = bp; + } + javabp.vsbp.order = idx; + javabp_arr.push(javabp); + }). + then(javabp => _setup_breakpoints(o, ++idx, javabp_arr)); + }; - // send back the actual breakpoint positions - response.body = { - breakpoints: breakpoints - }; - this.sendResponse(response); + if (!this._set_breakpoints_queue) { + this._set_breakpoints_queue = { + _dbgr:this, + _queue:[], + add(item) { + if (this._queue.push(item) > 1) return; + this._next(); + }, + _setup_breakpoints: _setup_breakpoints, + _next() { + if (!this._queue.length) return; // done + 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); + // .. and do the next one + this._queue.shift(); + this._next(); + }); + }, + }; + } + + this._set_breakpoints_queue.add({args,response,relative_fpn}); } setExceptionBreakPointsRequest(response /*: SetExceptionBreakpointsResponse*/, args /*: SetExceptionBreakpointsArguments*/) { diff --git a/src/debugger.js b/src/debugger.js index 6d3fe51..b6b0f2b 100644 --- a/src/debugger.js +++ b/src/debugger.js @@ -137,8 +137,8 @@ Debugger.prototype = { adbclient: null, stoppedlocation: null, classes: {}, - // classprepare notifier done - cpndone: false, + // classprepare filters + cpfilters: [], preparedclasses: [], } return this; @@ -502,7 +502,7 @@ Debugger.prototype = { var cls = this._splitsrcfpn(srcfpn); var bid = cls.qtype + ':' + line; var newbp = this.breakpoints.bysrcloc[bid]; - if (newbp) return newbp; + if (newbp) return $.Deferred().resolveWith(this, [newbp]); newbp = { id: bid, srcfpn: srcfpn, @@ -522,25 +522,25 @@ Debugger.prototype = { // what happens next depends upon what state we are in switch (this.status()) { case 'connected': - //this._changebpstate([newbp], 'set'); - //this._changebpstate([newbp], 'notloaded'); newbp.state = 'notloaded'; - if (this.session.cpndone) { - var bploc = this._findbplocation(this.session.classes, newbp); - if (bploc) { - this._setupbreakpointsevent([bploc]); - } - } - break; + // try and load the class - if the runtime hasn't loaded it yet, this will just return an empty classes object + return this._loadclzinfo('L'+newbp.qtype+';') + .then(classes => { + if (!Object.keys(classes).length) + return this._ensureClassPrepareForPackage(newbp.pkg); + var bploc = this._findbplocation(classes, newbp); + if (bploc) + return this._setupbreakpointsevent([bploc]); + }) + .then(() => newbp) case 'connecting': case 'disconnected': default: - //this._changebpstate([newbp], 'set'); newbp.state = 'set'; break; } - return newbp; + return $.Deferred().resolveWith(this, [newbp]); }, clearbreakpoint: function (srcfpn, line) { @@ -1456,29 +1456,28 @@ Debugger.prototype = { _initbreakpoints: function () { var deferreds = [{ dbgr: this }]; - var donetypes = {}; // reset any current associations this.breakpoints.enabled = {}; // set all the breakpoints to the notloaded state this._changebpstate(this.breakpoints.all, 'notloaded'); - // setup class prepare notifications for all the current packages + // setup class prepare notifications for all the packages associated with breakpoints // when each class is prepared, we initialise any breakpoints for it - for (var pkg in this.session.build.packages) { - try { - var def = this._setupclassprepareevent(pkg + '.*', _onclassprepared); - deferreds.push(def); - } catch (e) { - D('Ignoring additional class prepared notification for: ' + preppedclass.type.signature); - } - } + var cpdefs = this.breakpoints.all.map(bp => this._ensureClassPrepareForPackage(bp.pkg)); + deferreds = deferreds.concat(cpdefs); return $.when.apply($, deferreds).then(function (x) { - x.dbgr.session.cpndone = true; return $.Deferred().resolveWith(x.dbgr); }); + }, - function _onclassprepared(preppedclass) { + _ensureClassPrepareForPackage: function(pkg) { + var filter = pkg + '.*'; + if (this.session.cpfilters.includes(filter)) + return $.Deferred().resolveWith(this,[]); // already setup + + this.session.cpfilters.push(filter); + return this._setupclassprepareevent(filter, preppedclass => { // if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get // multiple notifications (which duplicates breakpoints, etc) if (this.session.preparedclasses.includes(preppedclass.type.signature)) { @@ -1502,6 +1501,7 @@ Debugger.prototype = { bplocs.push(bploc); } } + if (!bplocs.length) return; // set all the breakpoints in one go... return this._setupbreakpointsevent(bplocs); }) @@ -1509,7 +1509,7 @@ Debugger.prototype = { // when all the breakpoints for the newly-prepared type have been set... this._resumesilent(); }); - } + }); }, clearBreakOnExceptions: function(extra) {