|
1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. |
|
2 // Use of this source code is governed by a BSD-style license that can be |
|
3 // found in the LICENSE file. |
|
4 |
|
5 package org.chromium.sdk.internal.tools.v8; |
|
6 |
|
7 import java.util.EnumSet; |
|
8 import java.util.Set; |
|
9 import java.util.concurrent.Semaphore; |
|
10 import java.util.concurrent.TimeUnit; |
|
11 import java.util.concurrent.atomic.AtomicReference; |
|
12 import java.util.logging.Level; |
|
13 import java.util.logging.Logger; |
|
14 |
|
15 import org.chromium.sdk.DebugEventListener; |
|
16 import org.chromium.sdk.TabDebugEventListener; |
|
17 import org.chromium.sdk.internal.BrowserImpl; |
|
18 import org.chromium.sdk.internal.BrowserTabImpl; |
|
19 import org.chromium.sdk.internal.DebugSession; |
|
20 import org.chromium.sdk.internal.DebugSessionManager; |
|
21 import org.chromium.sdk.internal.JsonUtil; |
|
22 import org.chromium.sdk.internal.Result; |
|
23 import org.chromium.sdk.internal.V8ContextFilter; |
|
24 import org.chromium.sdk.internal.protocol.data.ContextData; |
|
25 import org.chromium.sdk.internal.protocol.data.ContextHandle; |
|
26 import org.chromium.sdk.internal.protocolparser.JsonProtocolParseException; |
|
27 import org.chromium.sdk.internal.tools.ChromeDevToolsProtocol; |
|
28 import org.chromium.sdk.internal.tools.ToolHandler; |
|
29 import org.chromium.sdk.internal.tools.ToolOutput; |
|
30 import org.chromium.sdk.internal.tools.v8.request.DebuggerMessage; |
|
31 import org.chromium.sdk.internal.transport.Message; |
|
32 import org.json.simple.JSONObject; |
|
33 import org.json.simple.parser.ParseException; |
|
34 |
|
35 /** |
|
36 * Handles the interaction with the "V8Debugger" tool. |
|
37 */ |
|
38 public class ChromeDevToolSessionManager implements DebugSessionManager { |
|
39 |
|
40 /** |
|
41 * This exception is thrown whenever the handler could not get a tab |
|
42 * attachment result from the debugged browser. |
|
43 */ |
|
44 public static class AttachmentFailureException extends Exception { |
|
45 |
|
46 private static final long serialVersionUID = 1L; |
|
47 |
|
48 public AttachmentFailureException() { |
|
49 } |
|
50 |
|
51 public AttachmentFailureException(String message, Throwable cause) { |
|
52 super(message, cause); |
|
53 } |
|
54 } |
|
55 |
|
56 /** |
|
57 * An interface to run callbacks in response to V8 debugger commands that do |
|
58 * not have associated JSON payloads. |
|
59 */ |
|
60 public interface ResultAwareCallback { |
|
61 |
|
62 /** |
|
63 * This method is invoked whenever a response to a V8 command is received. |
|
64 * |
|
65 * @param result of the command |
|
66 */ |
|
67 void resultReceived(Result result); |
|
68 } |
|
69 |
|
70 /** The class logger. */ |
|
71 private static final Logger LOGGER = |
|
72 Logger.getLogger(ChromeDevToolSessionManager.class.getName()); |
|
73 |
|
74 private static final V8ContextFilter CONTEXT_FILTER = new V8ContextFilter() { |
|
75 public boolean isContextOurs(ContextHandle contextHandle) { |
|
76 Object data = contextHandle.data(); |
|
77 if (data == null) { |
|
78 return false; |
|
79 } |
|
80 |
|
81 final boolean skipSketchCode = true; |
|
82 if (skipSketchCode) { |
|
83 // We do not actually have a context id to compare with. So we shouldn't waste time |
|
84 // on parsing until we have this id. |
|
85 return true; |
|
86 } |
|
87 |
|
88 long scriptContextId; |
|
89 if (data instanceof String) { |
|
90 String stringData = (String) data; |
|
91 // we should parse string and check context id. It should have the format "type,id". |
|
92 } else if (data instanceof JSONObject) { |
|
93 JSONObject dataObject = (JSONObject) data; |
|
94 ContextData contextData; |
|
95 try { |
|
96 contextData = V8ProtocolUtil.getV8Parser().parse(dataObject, ContextData.class); |
|
97 } catch (JsonProtocolParseException e) { |
|
98 throw new RuntimeException(e); |
|
99 } |
|
100 scriptContextId = contextData.value(); |
|
101 } |
|
102 //TODO(peter.rybin): Here we are probably supposed to compare it with our context id. |
|
103 return true; |
|
104 } |
|
105 }; |
|
106 |
|
107 /** The host BrowserTabImpl instance. */ |
|
108 private final BrowserTabImpl browserTabImpl; |
|
109 |
|
110 private final ToolOutput toolOutput; |
|
111 |
|
112 /** The debug context for this handler. */ |
|
113 private final DebugSession debugSession; |
|
114 |
|
115 private final AtomicReference<AttachState> attachState = |
|
116 new AtomicReference<AttachState>(null); |
|
117 |
|
118 private final AtomicReference<ResultAwareCallback> attachCallback = |
|
119 new AtomicReference<ResultAwareCallback>(null); |
|
120 |
|
121 private final AtomicReference<ResultAwareCallback> detachCallback = |
|
122 new AtomicReference<ResultAwareCallback>(null); |
|
123 |
|
124 /** |
|
125 * A no-op JavaScript to evaluate. |
|
126 */ |
|
127 public static final String JAVASCRIPT_VOID = "javascript:void(0);"; |
|
128 |
|
129 public ChromeDevToolSessionManager(BrowserTabImpl browserTabImpl, ToolOutput toolOutput) { |
|
130 this.browserTabImpl = browserTabImpl; |
|
131 this.toolOutput = toolOutput; |
|
132 V8CommandOutputImpl v8MessageOutput = new V8CommandOutputImpl(toolOutput); |
|
133 this.debugSession = new DebugSession(this, CONTEXT_FILTER, v8MessageOutput); |
|
134 } |
|
135 |
|
136 public DebugSession getDebugSession() { |
|
137 return debugSession; |
|
138 } |
|
139 |
|
140 public DebugEventListener getDebugEventListener() { |
|
141 return browserTabImpl.getDebugEventListener(); |
|
142 } |
|
143 |
|
144 private TabDebugEventListener getTabDebugEventListener() { |
|
145 return browserTabImpl.getTabDebugEventListener(); |
|
146 } |
|
147 |
|
148 private void handleChromeDevToolMessage(final Message message) { |
|
149 JSONObject json; |
|
150 try { |
|
151 json = JsonUtil.jsonObjectFromJson(message.getContent()); |
|
152 } catch (ParseException e) { |
|
153 LOGGER.log(Level.SEVERE, "Invalid JSON received: {0}", message.getContent()); |
|
154 return; |
|
155 } |
|
156 String commandString = JsonUtil.getAsString(json, ChromeDevToolsProtocol.COMMAND.key); |
|
157 DebuggerToolCommand command = DebuggerToolCommand.forName(commandString); |
|
158 if (command != null) { |
|
159 switch (command) { |
|
160 case ATTACH: |
|
161 processAttach(json); |
|
162 break; |
|
163 case DETACH: |
|
164 processDetach(json); |
|
165 break; |
|
166 case DEBUGGER_COMMAND: |
|
167 processDebuggerCommand(json); |
|
168 break; |
|
169 case NAVIGATED: |
|
170 processNavigated(json); |
|
171 break; |
|
172 case CLOSED: |
|
173 processClosed(json); |
|
174 break; |
|
175 } |
|
176 return; |
|
177 } |
|
178 throw new IllegalArgumentException("Invalid command: " + commandString); |
|
179 } |
|
180 |
|
181 public void onDebuggerDetached() { |
|
182 // ignore |
|
183 } |
|
184 |
|
185 /** |
|
186 * Disconnects tab from connections. Can safely be called several times. This sends EOS |
|
187 * message and unregisters tab from browser. |
|
188 * Should be called from UI, may block. |
|
189 */ |
|
190 public void cutTheLineMyself() { |
|
191 toolHandler.cutTheLine(); |
|
192 } |
|
193 /** |
|
194 * This method is sure to be called only once. |
|
195 */ |
|
196 private void handleEos() { |
|
197 // We overwrite other values; nobody else should become unhappy, all expecters |
|
198 // are in handle* methods and they are not going to get their results |
|
199 attachState.set(AttachState.DISCONNECTED); |
|
200 browserTabImpl.handleEosFromToolService(); |
|
201 debugSession.getV8CommandProcessor().processEos(); |
|
202 |
|
203 DebugEventListener debugEventListener = getDebugEventListener(); |
|
204 if (debugEventListener != null) { |
|
205 debugEventListener.disconnected(); |
|
206 } |
|
207 } |
|
208 |
|
209 private AttachState getAttachState() { |
|
210 return attachState.get(); |
|
211 } |
|
212 |
|
213 /** |
|
214 * This method is for UI -- pretty low precision of result type. |
|
215 * @return whether the handler is attached to a tab |
|
216 */ |
|
217 public boolean isAttachedForUi() { |
|
218 return STATES_CALLED_ATTACHED.contains(getAttachState()); |
|
219 } |
|
220 |
|
221 /** |
|
222 * Attaches the remote debugger to the associated browser tab. |
|
223 * |
|
224 * @return the attachment result |
|
225 * @throws AttachmentFailureException whenever the handler could not connect |
|
226 * to the browser |
|
227 */ |
|
228 public Result attachToTab() throws AttachmentFailureException { |
|
229 boolean res = attachState.compareAndSet(null, AttachState.ATTACHING); |
|
230 if (!res) { |
|
231 throw new AttachmentFailureException("Illegal state", null); |
|
232 } |
|
233 |
|
234 String command = V8DebuggerToolMessageFactory.attach(); |
|
235 Result attachResult = sendSimpleCommandSync(attachCallback, command); |
|
236 |
|
237 debugSession.startCommunication(); |
|
238 |
|
239 return attachResult; |
|
240 } |
|
241 |
|
242 /** |
|
243 * Detaches the remote debugger from the associated browser tab. |
|
244 * Should be called from UI thread. |
|
245 * @return the detachment result |
|
246 */ |
|
247 public Result detachFromTab() { |
|
248 if (attachState.get() != AttachState.NORMAL) { |
|
249 toolHandler.onDebuggerDetached(); |
|
250 return Result.ILLEGAL_TAB_STATE; |
|
251 } |
|
252 |
|
253 String command = V8DebuggerToolMessageFactory.detach(); |
|
254 Result result; |
|
255 try { |
|
256 result = sendSimpleCommandSync(detachCallback, command); |
|
257 } catch (AttachmentFailureException e) { |
|
258 result = null; |
|
259 } finally { |
|
260 // Make sure line is cut |
|
261 cutTheLineMyself(); |
|
262 } |
|
263 |
|
264 return result; |
|
265 } |
|
266 |
|
267 private Result sendSimpleCommandSync(AtomicReference<ResultAwareCallback> callbackReference, |
|
268 String command) throws AttachmentFailureException { |
|
269 final Semaphore sem = new Semaphore(0); |
|
270 final Result[] output = new Result[1]; |
|
271 ResultAwareCallback callback = new ResultAwareCallback() { |
|
272 public void resultReceived(Result result) { |
|
273 output[0] = result; |
|
274 sem.release(); |
|
275 } |
|
276 }; |
|
277 |
|
278 boolean res = callbackReference.compareAndSet(null, callback); |
|
279 if (!res) { |
|
280 throw new IllegalStateException("Callback is already set"); |
|
281 } |
|
282 |
|
283 boolean completed; |
|
284 try { |
|
285 toolOutput.send(command); |
|
286 |
|
287 try { |
|
288 completed = sem.tryAcquire(BrowserImpl.OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
|
289 } catch (InterruptedException e) { |
|
290 throw new RuntimeException(e); |
|
291 } |
|
292 } finally { |
|
293 // Make sure we do not leave our callback behind us. |
|
294 callbackReference.compareAndSet(callback, null); |
|
295 } |
|
296 |
|
297 // If the command fails, notify the caller. |
|
298 if (!completed) { |
|
299 throw new AttachmentFailureException("Timed out", null); |
|
300 } |
|
301 |
|
302 return output[0]; |
|
303 } |
|
304 |
|
305 public ToolHandler getToolHandler() { |
|
306 return toolHandler; |
|
307 } |
|
308 |
|
309 private final ToolHandlerImpl toolHandler = new ToolHandlerImpl(); |
|
310 |
|
311 private class ToolHandlerImpl implements ToolHandler { |
|
312 private volatile boolean isLineCut = false; |
|
313 private boolean alreadyHasEos = false; |
|
314 |
|
315 /** |
|
316 * Here we call methods that are normally are being invoked from Connection Dispatch |
|
317 * thread. So we compete for "this" as synchronization monitor with that thread. |
|
318 * We should be careful, cause theoretically it may cause a deadlock. |
|
319 */ |
|
320 void cutTheLine() { |
|
321 // First mark ourselves as "cut off" to stop other threads, |
|
322 // then start waiting on synchronized. |
|
323 isLineCut = true; |
|
324 synchronized (this) { |
|
325 sendEos(); |
|
326 } |
|
327 } |
|
328 |
|
329 public synchronized void handleMessage(Message message) { |
|
330 if (isLineCut) { |
|
331 return; |
|
332 } |
|
333 handleChromeDevToolMessage(message); |
|
334 } |
|
335 |
|
336 public synchronized void handleEos() { |
|
337 if (isLineCut) { |
|
338 return; |
|
339 } |
|
340 sendEos(); |
|
341 } |
|
342 private synchronized void sendEos() { |
|
343 if (alreadyHasEos) { |
|
344 return; |
|
345 } |
|
346 alreadyHasEos = true; |
|
347 ChromeDevToolSessionManager.this.handleEos(); |
|
348 } |
|
349 |
|
350 public void onDebuggerDetached() { |
|
351 // ignore |
|
352 } |
|
353 } |
|
354 |
|
355 private void processClosed(JSONObject json) { |
|
356 notifyCallback(detachCallback, Result.OK); |
|
357 getTabDebugEventListener().closed(); |
|
358 cutTheLineMyself(); |
|
359 } |
|
360 |
|
361 /** |
|
362 * This method is invoked from synchronized code sections. It checks if there is a callback |
|
363 * provided in {@code callbackReference}. Sets callback to null. |
|
364 * |
|
365 * @param callbackReference reference which may hold callback |
|
366 * @param result to notify the callback with |
|
367 */ |
|
368 private void notifyCallback(AtomicReference<ResultAwareCallback> callbackReference, |
|
369 Result result) { |
|
370 ResultAwareCallback callback = callbackReference.getAndSet(null); |
|
371 if (callback != null) { |
|
372 try { |
|
373 callback.resultReceived(result); |
|
374 } catch (Exception e) { |
|
375 LOGGER.log(Level.WARNING, "Exception in the callback", e); |
|
376 } |
|
377 } |
|
378 } |
|
379 |
|
380 private void processAttach(JSONObject json) { |
|
381 Long resultValue = JsonUtil.getAsLong(json, ChromeDevToolsProtocol.RESULT.key); |
|
382 Result result = Result.forCode(resultValue.intValue()); |
|
383 // Message destination equals context.getTabId() |
|
384 if (result == Result.OK) { |
|
385 boolean res = attachState.compareAndSet(AttachState.ATTACHING, AttachState.NORMAL); |
|
386 if (!res) { |
|
387 throw new IllegalStateException(); |
|
388 } |
|
389 } else { |
|
390 if (result == null) { |
|
391 result = Result.DEBUGGER_ERROR; |
|
392 } |
|
393 } |
|
394 notifyCallback(attachCallback, result); |
|
395 } |
|
396 |
|
397 private void processDetach(JSONObject json) { |
|
398 Long resultValue = JsonUtil.getAsLong(json, ChromeDevToolsProtocol.RESULT.key); |
|
399 Result result = Result.forCode(resultValue.intValue()); |
|
400 if (result == Result.OK) { |
|
401 // ignore result, we may already be in DISCONNECTED state |
|
402 attachState.compareAndSet(AttachState.DETACHING, AttachState.DETACHED); |
|
403 } else { |
|
404 if (result == null) { |
|
405 result = Result.DEBUGGER_ERROR; |
|
406 } |
|
407 } |
|
408 notifyCallback(detachCallback, result); |
|
409 } |
|
410 |
|
411 private void processDebuggerCommand(JSONObject json) { |
|
412 JSONObject v8Json = JsonUtil.getAsJSON(json, ChromeDevToolsProtocol.DATA.key); |
|
413 V8CommandProcessor.checkNull(v8Json, "'data' field not found"); |
|
414 debugSession.getV8CommandProcessor().processIncomingJson(v8Json); |
|
415 } |
|
416 |
|
417 private void processNavigated(JSONObject json) { |
|
418 String newUrl = JsonUtil.getAsString(json, ChromeDevToolsProtocol.DATA.key); |
|
419 debugSession.navigated(); |
|
420 getTabDebugEventListener().navigated(newUrl); |
|
421 } |
|
422 |
|
423 public static class V8CommandOutputImpl implements V8CommandOutput { |
|
424 private final ToolOutput toolOutput; |
|
425 |
|
426 public V8CommandOutputImpl(ToolOutput toolOutput) { |
|
427 this.toolOutput = toolOutput; |
|
428 } |
|
429 |
|
430 public void send(DebuggerMessage debuggerMessage, boolean isImmediate) { |
|
431 toolOutput.send( |
|
432 V8DebuggerToolMessageFactory.debuggerCommand( |
|
433 JsonUtil.streamAwareToJson(debuggerMessage))); |
|
434 if (isImmediate) { |
|
435 toolOutput.send( |
|
436 V8DebuggerToolMessageFactory.evaluateJavascript(JAVASCRIPT_VOID)); |
|
437 } |
|
438 } |
|
439 } |
|
440 |
|
441 private static class V8DebuggerToolMessageFactory { |
|
442 |
|
443 static String attach() { |
|
444 return createDebuggerMessage(DebuggerToolCommand.ATTACH, null); |
|
445 } |
|
446 |
|
447 static String detach() { |
|
448 return createDebuggerMessage(DebuggerToolCommand.DETACH, null); |
|
449 } |
|
450 |
|
451 public static String debuggerCommand(String json) { |
|
452 return createDebuggerMessage(DebuggerToolCommand.DEBUGGER_COMMAND, json); |
|
453 } |
|
454 |
|
455 public static String evaluateJavascript(String javascript) { |
|
456 return createDebuggerMessage(DebuggerToolCommand.EVALUATE_JAVASCRIPT, |
|
457 JsonUtil.quoteString(javascript)); |
|
458 } |
|
459 |
|
460 private static String createDebuggerMessage( |
|
461 DebuggerToolCommand command, String dataField) { |
|
462 StringBuilder sb = new StringBuilder("{\"command\":\""); |
|
463 sb.append(command.commandName).append('"'); |
|
464 if (dataField != null) { |
|
465 sb.append(",\"data\":").append(dataField); |
|
466 } |
|
467 sb.append('}'); |
|
468 return sb.toString(); |
|
469 } |
|
470 } |
|
471 |
|
472 private enum AttachState { |
|
473 ATTACHING, NORMAL, DETACHING, DETACHED, DISCONNECTED |
|
474 } |
|
475 |
|
476 private static final Set<AttachState> STATES_CALLED_ATTACHED = EnumSet.of(AttachState.NORMAL); |
|
477 } |