aqubi+shin1

Recent site activity

デバッグ実行の実装

EclipsePluginにてデバッグ実行の実装を行なった時の方法のメモを書いておきます。
今回私が実装したのはAIR GEARでActionScript3のデバッグ実行になります。
普段の開発ではあまり行なわない所ですが、新しい言語が出て来て使いたいけどIDEがない...って時にはあるかも??
ということで、不十分な所などが多いと思いますが、同じ道を踏む方の少しでも役に立てば。

今回実装する箇所について

今回実装を行ないたい内容を一通りイメージしてみます。

ソースコードで、Breakpointを付けます。

デバッグボタンを押してデバッグ実行を開始します。

Breakpointを付けた場所で処理が中断し、該当のソースコードの行が選択状態になります。

Resumeボタンを押すと処理が再開されます。
Terminateボタンを押すとアプリケーションが終了します。
Step intoボタンを押すとメソッド中の処理に移動
Step overボタンを押すと次の行に移動

は動作させていきます。

Expression(評価式)で、値の内容も見れるようにします。

こんな感じで。
Javaのデバック実行と同様な操作でできたらいいな という感じです。

Breakpointの実装

Breakpointを実装には、
  • Ruler
  • Marker
  • Breakpoint
の作成が必要になります。では順に見て行きます。

Ruler

拡張ポイント:org.eclipse.ui.popupMenus の viewerContribution

まずは、Ruler。Rulerというのは、Breakpointがつく行ヘッダー部分です。

Rulerにて右クリックするとポップアップメニューがでてきますよね。ここでBreakpointを追加できるメニュー(Action)を追加します。


    <extension point="org.eclipse.ui.popupMenus">
    <viewerContribution
        id="net.sf.amateras.air.debug.EditPopupActions"
        targetID="#TextRulerContext">
        <action
            id="net.sf.amateras.air.debug.ruler.ManageBreakpointRulerActionDelegate"
            label="Add Breakpoint"
            menubarPath="debug"
            class="net.sf.amateras.air.debug.ruler.ManageBreakpointRulerActionDelegate">
        </action>
    </viewerContribution>
    </extension>

targetIDには#TextRulerContext, menubarPathにはdebugを指定しましょー。
net.sf.amateras.air.debug.ruler.ManageBreakpointRulerActionDelegate のクラスは org.eclipse.ui.texteditor.AbstractRulerActionDelegate を継承して作成します。

public class ManageBreakpointRulerActionDelegate extends AbstractRulerActionDelegate {
    @Override
    protected IAction createAction(ITextEditor editor, IVerticalRulerInfo rulerInfo) {
        return new BreakpointRulerAction(editor, rulerInfo);
    }
}

createActionで返すIActionは、org.eclipse.jface.action.Actionを継承してBreakpointのMarkerを追加する処理を書いていくことになります。...が、まだMarker,Breakpointを作成していないので、実装はまた後で。。

メニューでは、Breakpointがついていない行の場合にはAdd Breakpoint , 既についている行の場合は Remove Breakpoint というラベルを表示したいので、org.eclipse.ui.texteditor.IUpdateを実装してタイミングを取得し、Action#setTextに表示文字列を設定していけばOK.

上記を行なっても、Ruler部分をマウスでダブルクリックしてもBreakpointの取り外しはできないです。
この動作をさせるためには、また別途設定が必要になります。

    <extension point="org.eclipse.ui.editorActions">
        <editorContribution
            id="net.sf.amateras.air.debug.ruler.BreakpointRulerActions"
            targetID="net.sf.amateras.air.as.ActionScriptEditor">
            <action
                id="net.sf.amateras.air.debug.ruler.BreakpointRulerAction"
                actionID="RulerDoubleClick"
                label="Add breakpoint"
                class="net.sf.amateras.air.debug.ruler.ManageBreakpointRulerActionDelegate">
            </action>
        </editorContribution>
    </extension>

ActionのactionIDをRulerDoubleClickとするといいみたいです。
ClassはPopupメニュー時と同じクラスでOK!

Marker

拡張ポイント:org.eclipse.core.resources.markers

Marker(マーカー)というのは、ソースコードにマークを付けるものです。
コンパイルエラー時に表示されるようなものもMarkerの一種です。



   <extension
         id="net.sf.amateras.air.debug.markerType.lineBreakpoint"
         name="AIR Line Breakpoint Marker"
         point="org.eclipse.core.resources.markers">
      <super type="org.eclipse.debug.core.lineBreakpointMarker"/>
      <persistent value="true"/>
   </extension>

persistent を true にすると永続化するという定義になります。

マークのイメージを変える場合は、ImageProviders で設定ができるようです。
    <extension
        point="org.eclipse.ui.ide.markerImageProviders">
        <imageprovider
           icon="icons/breakmarker.gif"
           id="airBreakPointImage"
           markertype="net.sf.amateras.air.debug.markerType.lineBreakpoint"/>
    </extension>
私の場合、この定義が無くてもデフォルトの青い○のイメージが出て来たのでこの設定はしてません。。

Breakpoint

拡張ポイント:org.eclipse.debug.core.breakpoints

  <extension
         point="org.eclipse.debug.core.breakpoints">
      <breakpoint
            class="net.sf.amateras.air.debug.AirLineBreakPoint"
            name="AIR Line Breakpoints"
            markerType="net.sf.amateras.air.debug.markerType.lineBreakpoint"
            id="net.sf.amateras.air.debug.lineBreakpoint"/>
   </extension>

私の場合、Breakpointはorg.eclipse.debug.core.model.LineBreakpointを継承して作成しました。
Breakpointは作成されるとBreakpointManagerにて管理していきます。BreakpintManagerはDebugPlugin.getDefault().getBreakpointManager()で取得できます。
BreakpointManagerにて登録されていると、DebugPerspectiveのBreakpointにて表示されるようになります。

Breakpointは、Marker属性として表示文字列、ライン番号、有効/無効、永続化有無などを定義していきます。
        IMarker marker = resource.createMarker(AirLineBreakPoint.BREAK_POINT_MARKER);
        Map<String, Object> map = new HashMap<String, Object>(6);
        map.put(IMarker.MESSAGE, "AIR GEAR - BREAK POINT");
        map.put(IBreakpoint.ID, BREAK_POINT);
        map.put(IMarker.LINE_NUMBER, new Integer(lineNumber));
        map.put(IBreakpoint.PERSISTED, Boolean.TRUE);
        map.put(IBreakpoint.ENABLED, Boolean.TRUE);
        map.put(IBreakpoint.REGISTERED, Boolean.FALSE);
        marker.setAttributes(map);
        setMarker(this);

デバッグ実行開始後に設定されたBreakpointは、org.eclipse.debug.core.IBreakpointManagerListenerで、りすん可能です。
このリスナーはBreakpointManagerに追加するので、
DebugPlugin.getDefault().getBreakpointManager().addBreakpointManagerListener( IBreakpointManagerListenerの実装 );
でOK.

WatchExpression

拡張ポイント:org.eclipse.ui.popupMenus, org.eclipse.debug.core.watchExpressionDelegates

Expression(評価式)を追加する時は、追加する文字を選択状態にして、右クリックで表示されるポップアップメニューにします。

という事で、拡張ポイントはpopupMenus
   <extension point="org.eclipse.ui.popupMenus">
    <viewerContribution id="net.sf.amateras.air.debug.WatchExpression"
        targetID="#TextEditorContext">
        <action
            class="net.sf.amateras.air.debug.watch.WatchExpressionAction"
            id="net.sf.amateras.air.debug.watch.WatchExpression"
            menubarPath="additions"
            label="Watch">
         </action>
    </viewerContribution>
    </extension>

targetIDは#TextEditorContextです。

ちなみに、このターゲーットのID値はorg.eclipse.ui.editors.text.TextEditor#initializeEditorをみると
       setEditorContextMenuId("#TextEditorContext"); //$NON-NLS-1$
        setRulerContextMenuId("#TextRulerContext"); //$NON-NLS-1$
        setHelpContextId(ITextEditorHelpContextIds.TEXT_EDITOR);
なんてなっています。こぅいぅIDを用意して拡張しやすいようにしてくれてるって感じ。

ActionはIEditorActionDelegateを実装したクラスになります。この中でWatchExpressionを作成していきます。
デフォルトのクラスでOKなのであれば、以下で作成できます。
 IWatchExpression expression = DebugPlugin.getDefault().getExpressionManager().newWatchExpression(variable);

作成したIWatchExpressionのクラスをExpressionManagerに追加してあげるとBreakpoint同様にビューに表示されるようになります。

ListenerもBreakpoint同様、DebugPlugin.getDefault().getBreakpointManager().addExpressionListenerで、りすんできます。
評価式のViewが表示されていない時には、コードから表示させることもできます。こんな感じ。

        IWorkbenchPage page = AIRPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getActivePage();
        IViewPart part = page.findView(IDebugUIConstants.ID_EXPRESSION_VIEW);
        if (part == null) {
            try {
                page.showView(IDebugUIConstants.ID_EXPRESSION_VIEW);
            } catch (PartInitException e) {
                AIRPlugin.logException(e);
            }
        } else {
            page.bringToTop(part);
        }


評価を行なうために委譲クラスを設定して処理を組み込みます。
    <extension point="org.eclipse.debug.core.watchExpressionDelegates">
    <watchExpressionDelegate
        debugModel="net.sf.amateras.air.debug"
        delegateClass="net.sf.amateras.air.debug.watch.AirWatchExpressionDelegate"/>
    </extension>

このクラスは、org.eclipse.debug.core.model.IWatchExpressionDelegateのインターフェースを実装します。
実装が必要なメソッドは以下の一つです。
public void evaluateExpression(String expression, IDebugElement context, IWatchExpressionListener listener)

評価式って、まだ評価できてない状態 と 評価が出来てる状態 というのがあるのですが、IWatchExpressionListenerが使われます。
IWatchExpressionListener#watchEvaluationFinished のメソッドがCallされたら評価完了した状態になります。
このメソッドがCallされる前はデフォルトで名前の後ろに(pending)という文字が表示されます。


Launcherの設定

拡張ポイント:org.eclipse.debug.core.launchConfigurationTypes, org.eclipse.debug.ui.launchShortcuts

<extension
point="org.eclipse.debug.core.launchConfigurationTypes">
<launchConfigurationType
delegate="net.sf.amateras.air.launch.LaunchAIRConfiguration"
id="net.sf.amateras.air.launchAIRConfigurationType"
modes="run, debug"
name="AIR Application"
public="true">
</launchConfigurationType>
</extension>
modesにdebugを追加します。
org.eclipse.debug.ui.launchShortcutsも作成している場合には、こちらのmodeにも忘れずdebugを入れておきます。

デバッグ実行を行なうと、LaunchConfigurationDelegateで実装する以下のメソッド
public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch,
IProgressMonitor monitor) throws CoreException

の mode の値が ILaunchManager.DEBUG_MODE (=debug) の値になってきます。
これで条件分けして普通の実行と、デバッグ実行とで処理を分けて実装していきます。

AIR GEARでは以下の感じで実装。
if (mode.equals(ILaunchManager.DEBUG_MODE)) {
// debugの時
IDebugTarget debugTarget = new AirDebugTarget(launch, target);
launch.addDebugTarget(debugTarget);

} else {
// runの時
File executeFile = new File(sdkPath, adlpath);
String[] command = new String[] { target };
Process process = ProcessUtil.createProcess(executeFile, command, null);
DebugPlugin.newProcess(launch, process, "\"" + executeFile.getName() + " "
+ new File(command[0]).getName());
}
IDebugTargetのクラスをlaunchに追加します。
runの時のような実行する処理はIDebugTargetの中に書いていきます。

IDebugTargetのクラスの概要をみてみます。
デバッグ実行して、Debug perspective を表示すると以下のような画面がでてくるのを見た事あると思います。


上図の「AirDebugTarget」というのが、IDebugTargetの実装クラスです。
その子要素として org.eclipse.debug.core.model.IThread のクラス。(上図ではAir thread)
その子要素として org.eclipse.debug.core.model.IStackFrame のクラス (上図では airgear.as#add() [line:11])
があります。
ここのクラス達を実装していく必要があります。

IDebugTarget

org.eclipse.debug.core.model.IDebugTargetは、IDebugElementの他に以下のインターフェースを実装しています。
  • ITerminate
  • ISuspendResume
  • IDisconnect
これらは、名前からも想像つくように
この部分の実装です。
たとえば、Suspendの場合はISuspendResumeで実装な必要な以下のメソッドで...
public boolean canSuspend() で対象のボタンの有効/無効が切り変わり、
public boolean isSuspended() でDebugビューのTreeアイコンが変わり、
public void suspend() でボタンを押した時の処理として実行 
されます。

IDebugElementの実装が抽象クラスとしてorg.eclipse.debug.core.model.DebugElementが用意されているので、これを継承してクラスを作って行きます。
org.eclipse.debug.core.model.DebugElementは、以下のIThread,IStackFrameでも継承していきます。

子要素のIThreadは
public IThread[] getThreads() throws DebugException の実装になります。

IThread

org.eclipse.debug.core.model.IThread は、IDebugElement, ISuspendResume, IStep, ITerminateを継承したインターフェースです。
IStepは
の部分の実装になります。

ITerminateなど、IDebugTargetにもIThreadにもあります。
これはDebugビューで選択されているTreeによってどちらが参照されるかが決まります。
IDebugTargetのTreeノードが選択されていたら、IDebugTarget#isTerminated が参照されるし、IThreadのTreeノードが選択されていたら、IThread#isTerminaterdが参照されます。

子要素であるIStackFrameは、public IStackFrame[] getStackFrames() の実装で決まります。
IStackFrameの配列の順番ですが、実行中のものを1番目の要素にして、public IStackFrame getTopStackFrame() ではその1番目の要素を返すようにした方が無難と思うので、特に問題がなければそうしましょう。

IStackFrame

org.eclipse.debug.core.model.IStackFrameは、IThread同様、IDebugElement, ISuspendResume, IStep, ITerminateを継承したインターフェースになります。
それに加え、以下の情報を含んでいます。
  • Name
  • LineNo
  • Variables
  • RegisterGroups
IStackFrameは、中断しているソース、行番号、Variables(変数)などの単位のクラスになります。
このノードが選択された時、該当ソースの行番号に飛ぶ操作がおこなわれます。
ですが、ソースを探る部分については別のクラス....SourceLocatorの役割になっています。これは後述。。。

このクラスでは、public boolean equals(Object obj) とpublic int hashCode()を上書きして、equalsの実装をしてあげます。
そうしないと挙動不審な感じになりますww

SourceLocator

拡張ポイント:org.eclipse.debug.core.sourceLocators、org.eclipse.debug.core.sourcePathComputers

SourceLocatorは該当ソースを探る役割をしますが、クラス的には大きく2つの役割に分かれます
  • ソースがあるディレクトリを判別する
  • ディレクトリの中から、該当ソースを探す
ソースがあるディレクトリを判別する方は起動時に実行されます。
指定方法は、
   <extension
         point="org.eclipse.debug.core.sourcePathComputers">
      <sourcePathComputer
            class="net.sf.amateras.air.debug.sourcelocater.AirSourcePathComputerDelegate"
            id="debug.sourcePathComputer" />
   </extension>
とし、このidをlaunchConfigurationTypeの属性に
sourcePathComputerId="debug.sourcePathComputer"を追加します。

実装クラスは、org.eclipse.debug.core.sourcelookup.ISourcePathComputerDelegateを実装し、ISourceContainer[]を返します。
ISourceContainerは以下の実装があります。
  • org.eclipse.debug.core.sourcelookup.containers.ProjectSourceContainer
  • org.eclipse.debug.core.sourcelookup.containers.FolderSourceContainer
  • org.eclipse.debug.core.sourcelookup.containers.DirectorySourceContainer
  • org.eclipse.debug.core.sourcelookup.containers.WorkspaceSourceContainer
思うようなものがなければ、自作しましょぅ。

もう一つ、ディレクトリの中から該当ソースを探す方は以下の定義になります。
   <extension
         point="org.eclipse.debug.core.sourceLocators">
      <sourceLocator
            class="net.sf.amateras.air.debug.sourcelocater.AirSourceLookupDirector"
            name="AIR Source Locator"
            id="debug.sourceLocator"/>
   </extension>
このidをlaunchConfigurationTypeの属性に
sourceLocatorId="debug.sourceLocator" を追加します。

実装クラスは、org.eclipse.debug.core.sourcelookup.AbstractSourceLookupDirectorを継承して、org.eclipse.debug.ui.ISourcePresentationのインターフェースも実装します。

initializeParticipantsで、SourceLookupParticipantを追加します。
    public void initializeParticipants() {
        addParticipants(new ISourceLookupParticipant[] { new AirSourceLookupParticipant() });
    }
このクラスでは、public String getSourceName(Object object) を実装します。
ObjectにはIStackFrameが渡ってくるので、そこからソースの名前を返すようにします。

ISourcePresentationの実装があると、
public String getEditorId(IEditorInput input, Object element)
public IEditorInput getEditorInput(Object element)
のメソッドをEclipseから呼んでくれるようになります。

getEditorIdはEditorのIDを返します。
ファイルの種類によって、エディタを変えたい場合がありますよね。その時の対応です。

getEditorInputは対象となるEditorInputを返します。
AIR GEARの場合、Eclipse管理下ではないファイル... FlexSDKのファイル...も検索対象にしたいので、その場合にはIStorageEditorInputを実装したEditorInputを
自作して使っています。


状態の通知


実行した処理がSuspendになった、Resumeされた...というタイミングで、その状態を報告してあげる事によりEclipseのView上も動いてくれるようになります。
org.eclipse.debug.core.model.DebugElementを継承した、DebugTarget, Thread, StackFrame であれば、以下のメソッドを使用します。
  • public void fireChangeEvent(int detail)
  • public void fireCreationEvent()
  • public void fireResumeEvent(int detail)
  • public void fireSuspendEvent(int detail)
  • public void fireTerminateEvent()
int detailの引数は、org.eclipse.debug.core.DebugEventで切られている定数を使います。
例えばfireSuspendEventの場合、BreakpointによってSuspendされた場合には、DebugEvent.BREAKPOINT を渡します。