001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.ruby.debugger;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.io.File;
046: import java.util.logging.Level;
047: import org.netbeans.api.debugger.DebuggerManager;
048: import org.netbeans.api.debugger.DebuggerManagerAdapter;
049: import org.netbeans.api.debugger.DebuggerManagerListener;
050: import org.netbeans.api.debugger.Session;
051: import org.netbeans.modules.ruby.debugger.RubySession.State;
052: import org.netbeans.modules.ruby.debugger.model.CallSite;
053: import org.netbeans.modules.ruby.debugger.ui.CallStackAnnotation;
054: import org.netbeans.modules.ruby.platform.execution.FileLocator;
055: import org.netbeans.spi.debugger.SessionProvider;
056: import org.openide.filesystems.FileObject;
057: import org.openide.filesystems.FileUtil;
058: import org.openide.text.Line;
059: import org.rubyforge.debugcommons.RubyDebugEventListener;
060: import org.rubyforge.debugcommons.RubyDebuggerException;
061: import org.rubyforge.debugcommons.model.RubyThreadInfo;
062: import org.rubyforge.debugcommons.RubyDebuggerProxy;
063: import org.rubyforge.debugcommons.model.RubyFrame;
064: import org.rubyforge.debugcommons.model.RubyThread;
065: import org.rubyforge.debugcommons.model.RubyValue;
066: import org.rubyforge.debugcommons.model.RubyVariable;
067:
068: public final class RubySession {
069:
070: /**
071: * Used by the NetBeans META-INF tree to identify the language type session
072: * directory.
073: */
074: private final static String RUBY_SESSION = "RubySession"; // NOI18N
075:
076: static boolean TEST;
077:
078: private final RubyThreadInfo[] EMPTY_THREAD_INFOS = new RubyThreadInfo[0];
079: private final RubyFrame[] EMPTY_FRAMES = new RubyFrame[0];
080: private final RubyVariable[] EMPTY_VARIABLES = new RubyVariable[0];
081:
082: private final RubyDebuggerProxy proxy;
083: private final FileLocator fileLocator;
084: private RubyThread activeThread;
085: private RubyFrame selectedFrame;
086: private final DebuggerManagerListener sessionListener;
087: private State state;
088:
089: // package-private for tests only
090: File runningToFile;
091: int runningToLine;
092:
093: public enum State {
094: STARTING, RUNNING, STOPPED
095: };
096:
097: public RubySession(final RubyDebuggerProxy proxy,
098: final FileLocator fileLocator) {
099: this .proxy = proxy;
100: this .fileLocator = fileLocator;
101: this .sessionListener = new RubySessionListener();
102: this .state = State.STARTING;
103: this .runningToLine = -1;
104: DebuggerManager.getDebuggerManager().addDebuggerListener(
105: DebuggerManager.PROP_CURRENT_SESSION, sessionListener);
106: }
107:
108: public State getState() {
109: return state;
110: }
111:
112: public void resume() {
113: beforeProceed();
114: activeThread.resume();
115: EditorUtil.unmarkCurrent();
116: state = State.RUNNING;
117: }
118:
119: public void stepInto() {
120: try {
121: beforeProceed();
122: if (!activeThread.canStepInto()) {
123: return;
124: }
125: activeThread.stepInto(forceNewLine());
126: state = State.RUNNING;
127: } catch (RubyDebuggerException e) {
128: Util.severe("Cannot step into", e); // NOI18N
129: }
130: }
131:
132: public void stepOver() {
133: try {
134: beforeProceed();
135: if (!activeThread.canStepOver()) {
136: return;
137: }
138: activeThread.stepOver(forceNewLine());
139: state = State.RUNNING;
140: } catch (RubyDebuggerException e) {
141: Util.severe("Cannot step over", e); // NOI18N
142: }
143: }
144:
145: public void stepReturn() {
146: try {
147: beforeProceed();
148: activeThread.stepReturn();
149: state = State.RUNNING;
150: } catch (RubyDebuggerException e) {
151: Util.severe("Cannot step return", e); // NOI18N
152: }
153: }
154:
155: public void runToCursor() {
156: File file;
157: int line;
158: if (TEST) {
159: file = runningToFile;
160: line = runningToLine;
161: } else {
162: assert runningToFile == null : "runningToFile is not set";
163: beforeProceed();
164: Line eLine = EditorUtil.getCurrentLine();
165: if (eLine == null) {
166: return;
167: }
168: FileObject fo = eLine.getLookup().lookup(FileObject.class);
169: if (fo == null) {
170: return;
171: }
172: if (!Util.isRubySource(fo)) {
173: return;
174: }
175: file = FileUtil.toFile(fo);
176: line = eLine.getLineNumber() + 1;
177: }
178: if (file != null) {
179: try {
180: runningToFile = file;
181: runningToLine = line;
182: activeThread.runTo(file.getAbsolutePath(), line);
183: state = State.RUNNING;
184: } catch (RubyDebuggerException e) {
185: Util.severe("Cannot step return", e); // NOI18N
186: }
187: }
188: }
189:
190: public boolean isRunningTo(final File f, final int line) {
191: assert f != null : "isRunningTo is not passed null File arg";
192: return f.equals(runningToFile) && line == runningToLine;
193: }
194:
195: public void finish(final RubyDebugEventListener listener,
196: final boolean terminate) {
197: CallStackAnnotation.clearAnnotations();
198: DebuggerManager.getDebuggerManager().removeDebuggerListener(
199: sessionListener);
200: proxy.removeRubyDebugEventListener(listener);
201: if (terminate) {
202: proxy.finish(true);
203: }
204: }
205:
206: public String getName() {
207: return "localhost:" + proxy.getDebugTarged().getPort(); // NOI18N
208: }
209:
210: /**
211: * Returns latest known threads for this session.
212: */
213: public RubyThreadInfo[] getThreadInfos() {
214: try {
215: return proxy.checkConnection() ? proxy.readThreadInfo()
216: : EMPTY_THREAD_INFOS;
217: } catch (RubyDebuggerException e) {
218: if (proxy.checkConnection()) {
219: Util.LOGGER.log(Level.INFO,
220: "Cannot read thread information", e);
221: }
222: return EMPTY_THREAD_INFOS;
223: }
224: }
225:
226: /**
227: * Returns latest known frames for this session.
228: */
229: public RubyFrame[] getFrames() {
230: try {
231: return isSessionSuspended() ? activeThread.getFrames()
232: : EMPTY_FRAMES;
233: } catch (RubyDebuggerException e) {
234: Util.severe("Cannot read frames information", e); // NOI18N
235: return EMPTY_FRAMES;
236: }
237: }
238:
239: /**
240: * Return top stack frame for the currently suspended thread.
241: *
242: * @return stack frame instance or <code>null</code> if there is not any
243: * suspended thread at the time
244: */
245: private RubyFrame getTopFrame() throws RubyDebuggerException {
246: return isSessionSuspended() ? activeThread.getTopFrame() : null;
247: }
248:
249: /**
250: * Selected frame is used for evaluating variables in Local Variables view
251: * or expressions in Watches view.
252: */
253: public void selectFrame(final RubyFrame frame) {
254: this .selectedFrame = frame;
255: }
256:
257: private RubyFrame getSelectedFrame() {
258: try {
259: return selectedFrame == null ? getTopFrame()
260: : selectedFrame;
261: } catch (RubyDebuggerException e) {
262: Util.LOGGER.log(Level.INFO,
263: "Unable to read top stack frame", e); // NOI18N
264: return null;
265: }
266: }
267:
268: public boolean isSelectedFrame(final RubyFrame frame) {
269: return frame.equals(getSelectedFrame());
270: }
271:
272: public RubyVariable[] getGlobalVariables() {
273: try {
274: return isSessionSuspended() ? proxy.readGlobalVariables()
275: : EMPTY_VARIABLES;
276: } catch (RubyDebuggerException e) {
277: Util.LOGGER.log(Level.INFO,
278: "Cannot read global variables information", e); // NOI18N
279: return EMPTY_VARIABLES;
280: }
281: }
282:
283: /**
284: * Returns latest known variables for this session.
285: */
286: public RubyVariable[] getVariables() {
287: try {
288: RubyFrame frame = getSelectedFrame();
289: return frame == null ? EMPTY_VARIABLES : frame
290: .getVariables();
291: } catch (RubyDebuggerException e) {
292: Util.LOGGER.log(Level.INFO,
293: "Cannot read variables information", e); // NOI18N
294: return EMPTY_VARIABLES;
295: }
296: }
297:
298: public RubyVariable[] getChildren(RubyVariable parent) {
299: try {
300: RubyValue val = parent.getValue();
301: return val == null ? EMPTY_VARIABLES : val.getVariables();
302: } catch (RubyDebuggerException e) {
303: Util.severe("Cannot read variables information", e); // NOI18N
304: return EMPTY_VARIABLES;
305: }
306: }
307:
308: public RubyVariable inspectExpression(final String expression) {
309: try {
310: RubyFrame frame = getSelectedFrame();
311: return frame == null ? null : frame
312: .inspectExpression(expression);
313: } catch (RubyDebuggerException e) {
314: Util.finest("Unable to inspect expression [" + expression
315: + ']'); // NOI18N
316: return null;
317: }
318: }
319:
320: void suspend(final RubyThread thread,
321: final ContextProviderWrapper contextProvider) {
322: state = State.STOPPED;
323: runningToFile = null;
324: runningToLine = -1;
325: switchThread(thread, contextProvider);
326: }
327:
328: public void switchThread(final RubyThread thread,
329: final ContextProviderWrapper contextProvider) {
330: if (thread.isSuspended()) {
331: activeThread = thread;
332: try {
333: RubyFrame frame = getTopFrame();
334: if (frame == null) {
335: return;
336: }
337: EditorUtil.markCurrent(resolveAbsolutePath(frame
338: .getFile()), frame.getLine() - 1);
339: annotateCallStack(thread);
340: if (contextProvider != null) {
341: contextProvider.fireModelChanges();
342: }
343: } catch (RubyDebuggerException e) {
344: Util.severe("Cannot switch thread", e); // NOI18N
345: }
346: } else {
347: Util
348: .finest("Cannot switch to thread which is not suspended ["
349: + thread + "]");
350: }
351: }
352:
353: public void switchThread(final int threadID,
354: final ContextProviderWrapper contextProvider) {
355: RubyThread thread = proxy.getDebugTarged().getThreadById(
356: threadID);
357: if (thread != null) {
358: switchThread(thread, contextProvider);
359: }
360: }
361:
362: public boolean isActiveThread(int id) {
363: return activeThread != null && activeThread.getId() == id;
364: }
365:
366: /** Package-private for tests only. */
367: public boolean isSessionSuspended() {
368: return activeThread != null && activeThread.isSuspended();
369: }
370:
371: public String resolveAbsolutePath(final String path) {
372: if (new File(path).isAbsolute()) {
373: return path;
374: }
375: String result = null;
376: FileObject fo = fileLocator.find(path);
377: if (fo != null) {
378: File file = FileUtil.toFile(fo);
379: if (file != null && file.isFile()) {
380: result = file.getAbsolutePath();
381: }
382: }
383: if (result == null) {
384: Util.finest("Cannot resolve absolute path for: \"" + path
385: + '"'); // NOI18N
386: }
387: return result;
388: }
389:
390: public boolean isSuspended(final RubyThreadInfo ti) {
391: RubyThread thread = proxy.getDebugTarged().getThreadById(
392: ti.getId());
393: if (thread != null) {
394: return thread.isSuspended();
395: } else {
396: Util.warning("There is no thread for: " + ti);
397: return false; // 'default'
398: }
399: }
400:
401: private void annotateCallStack(final RubyThread thread) {
402: if (TEST)
403: return;
404: try {
405: RubyFrame[] frames = thread.getFrames();
406: assert frames.length > 0 : "thread has >0 frames";
407: CallSite[] callSites = new CallSite[frames.length - 1]; // minus first frame
408: for (int i = 1; i < frames.length; i++) {
409: RubyFrame frame = frames[i];
410: final CallSite site = new CallSite(
411: resolveAbsolutePath(frame.getFile()), frame
412: .getLine() - 1);
413: callSites[i - 1] = site;
414: }
415: CallStackAnnotation.annotate(callSites);
416: } catch (RubyDebuggerException e) {
417: Util.LOGGER.log(Level.WARNING,
418: "Cannot annotated current call stack", e);
419: }
420:
421: }
422:
423: private void refresh() {
424: if (isSessionSuspended()) {
425: switchThread(activeThread, null);
426: }
427: }
428:
429: private void beforeProceed() {
430: selectFrame(null);
431: CallStackAnnotation.clearAnnotations();
432: }
433:
434: /**
435: * ERB generates several instructions for a single line of template code. So
436: * this method returns <code>true</code> for the ERB templates.
437: */
438: private boolean forceNewLine() throws RubyDebuggerException {
439: RubyFrame frame = activeThread.getTopFrame();
440: assert frame != null;
441: String path = frame.getFile();
442: File f = FileUtil.normalizeFile(new File(path));
443: FileObject fo = f.isAbsolute() ? FileUtil.toFileObject(f)
444: : fileLocator.find(path);
445: return fo == null ? false : Util.isERBSource(fo);
446: }
447:
448: /** Package-private for Unit tests only. */
449: RubyDebuggerProxy getProxy() {
450: return proxy;
451: }
452:
453: public SessionProvider createSessionProvider() {
454: return new SessionProvider() {
455: public String getSessionName() {
456: return RubySession.this .getName();
457: }
458:
459: public String getLocationName() {
460: return "localhost"; // NOI18N
461: }
462:
463: public String getTypeID() {
464: return RUBY_SESSION;
465: }
466:
467: public Object[] getServices() {
468: return new Object[] {};
469: };
470: };
471: }
472:
473: private static class RubySessionListener extends
474: DebuggerManagerAdapter {
475:
476: @Override
477: public void propertyChange(PropertyChangeEvent evt) {
478: Session currentSession = DebuggerManager
479: .getDebuggerManager().getCurrentSession();
480: if (currentSession != null
481: && RubyDebuggerEngineProvider.RUBY_LANGUAGE
482: .equals(currentSession.getCurrentLanguage())) {
483: Util.getCurrentSession().refresh();
484: }
485: }
486: }
487:
488: }
|