/******************************************************************************* * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * *******************************************************************************/ package com.liferay.ide.portal.core.debug.fm; import com.liferay.ide.core.util.CoreUtil; import com.liferay.ide.portal.core.PortalCore; import com.liferay.ide.portal.core.debug.ILRDebugConstants; import com.liferay.ide.server.util.ServerUtil; import com.sun.jdi.Field; import com.sun.jdi.ThreadReference; import com.sun.jdi.Value; import freemarker.debug.Breakpoint; import freemarker.debug.DebuggedEnvironment; import freemarker.debug.Debugger; import freemarker.debug.DebuggerClient; import freemarker.debug.DebuggerListener; import freemarker.debug.EnvironmentSuspendedEvent; import java.net.Inet4Address; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IMarkerDelta; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; import org.eclipse.debug.core.DebugEvent; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.IBreakpointManager; import org.eclipse.debug.core.IDebugEventSetListener; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.model.IBreakpoint; import org.eclipse.debug.core.model.IDebugTarget; import org.eclipse.debug.core.model.ILineBreakpoint; import org.eclipse.debug.core.model.IMemoryBlock; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.IThread; import org.eclipse.jdt.debug.core.IJavaDebugTarget; import org.eclipse.jdt.internal.debug.core.model.JDIThread; import org.eclipse.osgi.util.NLS; /** * @author Gregory Amerson * @author Cindy Li */ @SuppressWarnings( "restriction" ) public class FMDebugTarget extends FMDebugElement implements IDebugTarget, IDebugEventSetListener, IPreferenceChangeListener { private static final FMStackFrame[] EMPTY_STACK_FRAMES = new FMStackFrame[0]; public static final String FM_TEMPLATE_SERVLET_CONTEXT = "_SERVLET_CONTEXT_"; //$NON-NLS-1$ private Debugger debuggerClient; private EventDispatchJob eventDispatchJob; private FMStackFrame[] fmStackFrames = EMPTY_STACK_FRAMES; private FMThread fmThread; private String host; private ILaunch launch; private String name; private IProcess process; private boolean suspended = false; private FMDebugTarget target; private boolean terminated = false; private IThread[] threads = new IThread[0]; class EventDispatchJob extends Job implements DebuggerListener { private boolean setup; public EventDispatchJob() { super( "Freemarker Event Dispatch" ); setSystem( true ); } public void environmentSuspended( final EnvironmentSuspendedEvent event ) throws RemoteException { final int lineNumber = event.getLine(); final ILineBreakpoint[] breakpoints = getEnabledLineBreakpoints(); boolean foundBreakpoint = false; for( IBreakpoint breakpoint : breakpoints ) { if( breakpoint instanceof ILineBreakpoint ) { ILineBreakpoint lineBreakpoint = (ILineBreakpoint) breakpoint; try { final int bpLineNumber = lineBreakpoint.getLineNumber(); final String templateName = breakpoint.getMarker().getAttribute( ILRDebugConstants.FM_TEMPLATE_NAME, "" ); final String remoteTemplateName = event.getTemplateName().replaceAll( FM_TEMPLATE_SERVLET_CONTEXT, "" ); if( bpLineNumber == lineNumber && remoteTemplateName.equals( templateName ) ) { final String frameName = templateName + " line: " + lineNumber; fmThread.setEnvironment( event.getEnvironment() ); fmThread.setThreadId( event.getThreadId() ); fmThread.setBreakpoints( new IBreakpoint[] { breakpoint } ); fmStackFrames = new FMStackFrame[] { new FMStackFrame( fmThread, frameName ) }; foundBreakpoint = true; break; } } catch( CoreException e ) { PortalCore.logError( "Unable to suspend at breakpoint", e ); } } } if( ! foundBreakpoint && fmThread.isStepping() ) { final Breakpoint stepBp = fmThread.getStepBreakpoint(); if( stepBp != null ) { String frameName = getDisplayableTemplateName( stepBp.getTemplateName() ) + " line: " + stepBp.getLine(); fmThread.setEnvironment( event.getEnvironment() ); fmThread.setBreakpoints( null ); fmThread.setStepping( false ); fmStackFrames = new FMStackFrame[] { new FMStackFrame( fmThread, frameName ) }; foundBreakpoint = true; } } if( foundBreakpoint ) { suspended( DebugEvent.BREAKPOINT ); } else { // lets not pause the remote environment if for some reason the breakpoints don't match. new Job( "resuming remote environment" ) { @SuppressWarnings( "rawtypes" ) @Override protected IStatus run( IProgressMonitor monitor ) { IStatus retval = Status.OK_STATUS; try { for( Iterator i = getDebuggerClient().getSuspendedEnvironments().iterator(); i.hasNext(); ) { DebuggedEnvironment e = (DebuggedEnvironment) i.next(); e.resume(); } } catch( RemoteException e ) { retval = PortalCore.createErrorStatus( "Could not resume after missing breakpoint", e ); } return retval; } }.schedule(); PortalCore.logError( "Could not find local breakpoint, resuming all remote environments" ); } } @Override protected IStatus run( IProgressMonitor monitor ) { while( ! isTerminated() ) { // try to connect to debugger Debugger debugger = getDebuggerClient(); if( debugger == null ) { try { Thread.sleep( 1000 ); } catch( InterruptedException e ) { } continue; } if( !setup ) { setup = setupDebugger(debugger); } synchronized( eventDispatchJob ) { try { wait(); } catch( InterruptedException e ) { } } } return Status.OK_STATUS; } private boolean setupDebugger( Debugger debugger ) { try { debugger.addDebuggerListener( eventDispatchJob ); FMDebugTarget.this.threads = new IThread[] { FMDebugTarget.this.fmThread }; final IBreakpoint[] localBreakpoints = getEnabledLineBreakpoints(); addRemoteBreakpoints( debugger, localBreakpoints ); } catch( RemoteException e ) { return false; } return true; } } /** * Constructs a new debug target in the given launch for * the associated FM debugger * @param server * * @param launch containing launch * @param process Portal VM */ public FMDebugTarget( String host, ILaunch launch, IProcess process ) { super( null ); this.target = this; this.host = host; this.launch = launch; this.process = process; this.fmThread = new FMThread( this.target ); this.eventDispatchJob = new EventDispatchJob(); this.eventDispatchJob.schedule(); DebugPlugin.getDefault().getBreakpointManager().addBreakpointListener( this ); DebugPlugin.getDefault().addDebugEventListener( this ); PortalCore.getPrefs().addPreferenceChangeListener( this ); } public void addRemoteBreakpoints( final Debugger debugger, final IBreakpoint bps[] ) throws RemoteException { final List<Breakpoint> remoteBps = new ArrayList<Breakpoint>(); for( IBreakpoint bp : bps ) { final int line = bp.getMarker().getAttribute( IMarker.LINE_NUMBER, -1 ); final String templateName = bp.getMarker().getAttribute( ILRDebugConstants.FM_TEMPLATE_NAME, null ); final String remoteTemplateName = createRemoteTemplateName( templateName ); if( ! CoreUtil.isNullOrEmpty( remoteTemplateName ) && line > -1 ) { remoteBps.add( new Breakpoint( remoteTemplateName, line ) ); } } final Job job = new Job( "add remote breakpoints" ) { @Override protected IStatus run( IProgressMonitor monitor ) { IStatus retval = null; for( Breakpoint bp : remoteBps ) { try { debugger.addBreakpoint( bp ); } catch( RemoteException e ) { retval = PortalCore.createErrorStatus( NLS.bind( "Could not add remote breakpoint: {0}:{1}", new Object[] { bp.getTemplateName(), bp.getLine() } ), e ); if( retval != Status.OK_STATUS ) { PortalCore.logError( retval.getMessage() ); } } } return Status.OK_STATUS; } }; job.schedule(); } /* * (non-Javadoc) * @see org.eclipse.debug.core.IBreakpointListener#breakpointAdded(org.eclipse.debug.core.model.IBreakpoint) */ public void breakpointAdded( IBreakpoint breakpoint ) { if( supportsBreakpoint( breakpoint ) && !this.launch.isTerminated() ) { try { Debugger debugger = getDebuggerClient(); if( debugger != null && breakpoint.isEnabled() ) { addRemoteBreakpoints( debugger, new IBreakpoint[] { breakpoint } ); } } catch( Exception e ) { PortalCore.logError( "Error adding breakpoint.", e ); } } } /* * (non-Javadoc) * @see org.eclipse.debug.core.IBreakpointListener#breakpointChanged(org.eclipse.debug.core.model.IBreakpoint, * org.eclipse.core.resources.IMarkerDelta) */ public void breakpointChanged( IBreakpoint breakpoint, IMarkerDelta delta ) { if( supportsBreakpoint( breakpoint ) && ! this.launch.isTerminated() ) { try { if( breakpoint.isEnabled() ) { breakpointAdded( breakpoint ); } else { breakpointRemoved( breakpoint, null ); } } catch( CoreException e ) { } } } /* * (non-Javadoc) * @see org.eclipse.debug.core.IBreakpointListener#breakpointRemoved(org.eclipse.debug.core.model.IBreakpoint, * org.eclipse.core.resources.IMarkerDelta) */ public void breakpointRemoved( IBreakpoint breakpoint, IMarkerDelta delta ) { if( supportsBreakpoint( breakpoint ) && ! this.launch.isTerminated() ) { removeRemoteBreakpoints( new IBreakpoint[] { breakpoint } ); } } /* * (non-Javadoc) * @see org.eclipse.debug.core.model.IDisconnect#canDisconnect() */ public boolean canDisconnect() { return false; } public boolean canResume() { return !isTerminated() && isSuspended(); } /* * (non-Javadoc) * @see org.eclipse.debug.core.model.ISuspendResume#canSuspend() */ public boolean canSuspend() { return false; } public boolean canTerminate() { return false; } private void cleanup() { this.terminated = true; this.suspended = false; DebugPlugin.getDefault().getBreakpointManager().removeBreakpointListener(this); DebugPlugin.getDefault().removeDebugEventListener( this ); PortalCore.getPrefs().removePreferenceChangeListener( this ); } private String createRemoteTemplateName( String templateName ) { String retval = null; if( ! CoreUtil.isNullOrEmpty( templateName ) ) { final IPath templatePath = new Path( templateName ); final String firstSegment = templatePath.segment( 0 ); final IProject project = ServerUtil.findProjectByContextName( firstSegment ); if( project != null ) { /* * need to add special uri to the end of first segment of template path in order to make it work with * remote debugger */ final Object[] bindings = new Object[] { firstSegment, FM_TEMPLATE_SERVLET_CONTEXT, templatePath.removeFirstSegments( 1 ) }; retval = NLS.bind( "{0}{1}/{2}", bindings ); } else { retval = templatePath.toPortableString(); } } return retval; } /* * (non-Javadoc) * @see org.eclipse.debug.core.model.IDisconnect#disconnect() */ public void disconnect() throws DebugException { } public Debugger getDebuggerClient() { if( this.debuggerClient == null ) { try { this.debuggerClient = DebuggerClient.getDebugger( Inet4Address.getByName( this.host ), getDebugPort(), getDebugPassword() ); } catch( Exception e ) { } } return this.debuggerClient; } private String getDebugPassword() { String debugPassword = launch.getAttribute( PortalCore.PREF_FM_DEBUG_PASSWORD ); if( debugPassword != null ) { return debugPassword; } return PortalCore.getPreference( PortalCore.PREF_FM_DEBUG_PASSWORD ); } private int getDebugPort() { String debugPort = launch.getAttribute( PortalCore.PREF_FM_DEBUG_PORT ); if( debugPort != null ) { return Integer.parseInt( debugPort ); } return Integer.parseInt( PortalCore.getPreference( PortalCore.PREF_FM_DEBUG_PORT ) ); } public FMDebugTarget getDebugTarget() { return this.target; } private String getDisplayableTemplateName( String templateName ) { return templateName.replaceAll( FM_TEMPLATE_SERVLET_CONTEXT, "" ); } private ILineBreakpoint[] getEnabledLineBreakpoints() { List<ILineBreakpoint> breakpoints = new ArrayList<ILineBreakpoint>(); final IBreakpointManager breakpointManager = DebugPlugin.getDefault().getBreakpointManager(); if( breakpointManager.isEnabled() ) { IBreakpoint[] fmBreakpoints = breakpointManager.getBreakpoints( getModelIdentifier() ); for( IBreakpoint fmBreakpoint : fmBreakpoints ) { try { if( fmBreakpoint instanceof ILineBreakpoint && supportsBreakpoint( fmBreakpoint ) && fmBreakpoint.isEnabled() ) { breakpoints.add( (ILineBreakpoint) fmBreakpoint ); } } catch( CoreException e ) { } } } return breakpoints.toArray( new ILineBreakpoint[0] ); } public ILaunch getLaunch() { return this.launch; } /* * (non-Javadoc) * @see org.eclipse.debug.core.model.IMemoryBlockRetrieval#getMemoryBlock(long, long) */ public IMemoryBlock getMemoryBlock( long startAddress, long length ) throws DebugException { return null; } public String getName() throws DebugException { if( this.name == null ) { this.name = "Freemarker Debugger at " + this.host + ":" + getDebugPort(); } return this.name; } public IProcess getProcess() { return this.process; } FMStackFrame[] getStackFrames() { return this.fmStackFrames; } public IThread[] getThreads() throws DebugException { return this.threads; } public void handleDebugEvents( DebugEvent[] events ) { for( DebugEvent event : events ) { if( event.getKind() == DebugEvent.TERMINATE ) { if( this.process.equals( event.getSource() ) ) { if( this.process.isTerminated() ) { cleanup(); } } } } } public boolean hasThreads() throws DebugException { return this.threads != null && this.threads.length > 0; } /* * (non-Javadoc) * @see org.eclipse.debug.core.model.IDisconnect#isDisconnected() */ public boolean isDisconnected() { return false; } /* * (non-Javadoc) * @see org.eclipse.debug.core.model.ISuspendResume#isSuspended() */ public boolean isSuspended() { return this.suspended; } public boolean isTerminated() { return this.terminated || getProcess().isTerminated(); } public void preferenceChange( PreferenceChangeEvent event ) { if( PortalCore.PREF_ADVANCED_VARIABLES_VIEW.equals( event.getKey() ) ) { for( FMStackFrame stackFrame : getStackFrames() ) { stackFrame.clearVariables(); } } } private void removeRemoteBreakpoints( final IBreakpoint[] breakpoints ) { final List<Breakpoint> remoteBreakpoints = new ArrayList<Breakpoint>(); for( IBreakpoint bp : breakpoints ) { final String templateName = bp.getMarker().getAttribute( ILRDebugConstants.FM_TEMPLATE_NAME, "" ); final String remoteTemplateName =createRemoteTemplateName( templateName ); final Breakpoint remoteBp = new Breakpoint( remoteTemplateName, bp.getMarker().getAttribute( IMarker.LINE_NUMBER, -1 ) ); remoteBreakpoints.add( remoteBp ); } final Job job = new Job( "remove remote breakpoints" ) { @Override protected IStatus run( IProgressMonitor monitor ) { IStatus retval = null; for( Breakpoint bp : remoteBreakpoints ) { try { getDebuggerClient().removeBreakpoint( bp ); } catch( Exception e ) { retval = PortalCore.createErrorStatus( NLS.bind( "Unable to get debug client to remove breakpoint: {0}:{1}", new Object[] { bp.getTemplateName(), bp.getLine() } ), e ); if( retval != Status.OK_STATUS ) { PortalCore.logError( retval.getMessage() ); } } } return Status.OK_STATUS; } }; job.schedule(); } @SuppressWarnings( "rawtypes" ) public void resume() { final Job job = new Job("resume") { @Override protected IStatus run( IProgressMonitor monitor ) { try { // need to check to see if current thread is stepping and then remove the step breakpoint Debugger debugger = getDebuggerClient(); if( debugger != null ) { if( fmThread.isStepping() ) { Breakpoint stepBp = fmThread.getStepBreakpoint(); debugger.removeBreakpoint( stepBp ); } for( Iterator i = debugger.getSuspendedEnvironments().iterator(); i.hasNext(); ) { DebuggedEnvironment env = (DebuggedEnvironment) i.next(); try { env.resume(); } catch( Exception e ) { PortalCore.logError( "Could not resume suspended environment", e ); } } fmStackFrames = EMPTY_STACK_FRAMES; resumed( DebugEvent.CLIENT_REQUEST ); } } catch( RemoteException e ) { PortalCore.logError( "Could not fully resume suspended environments", e ); } return Status.OK_STATUS; } }; job.schedule(); } public void resume( FMThread thread ) throws DebugException { try { Breakpoint stepBp = thread.getStepBreakpoint(); if( stepBp != null ) { getDebuggerClient().removeBreakpoint( stepBp ); thread.setStepping( false ); thread.setStepBreakpoint( null ); } thread.getEnvironment().resume(); this.fmStackFrames = EMPTY_STACK_FRAMES; resumed( DebugEvent.CLIENT_REQUEST ); } catch( RemoteException e ) { throw new DebugException( PortalCore.createErrorStatus( e ) ); } } /** * Notification the target has resumed for the given reason * * @param detail * reason for the resume */ private void resumed( int detail ) { this.suspended = false; this.fmStackFrames = EMPTY_STACK_FRAMES; this.fmThread.fireResumeEvent( detail ); this.fireResumeEvent( detail ); } /* * Since current fm debugger doens't have native stepping we must emulate stepping with following steps. * * 1. Starting at the current stopped line, continue going down the template file to find a * suitable line to stop, ie, a addBreakpoint() that doesn't throw an exception. * 2. For the next line if there is already a breakpoint, simply call resume(), * 3. If there is no breakpoint already installed, add another one to the next line if that line has a valid * breakpoint location, then resume(). * 4. Once the next breakpoint is hit, we need to remove the previously added step breakpoint */ @SuppressWarnings( { "rawtypes" } ) void step( FMThread thread ) throws DebugException { int currentLineNumber = -1; String templateName = null; Breakpoint existingStepBp = null; final IBreakpoint[] breakpoints = thread.getBreakpoints(); if( breakpoints.length > 0 ) { try { ILineBreakpoint bp = (ILineBreakpoint) breakpoints[0]; currentLineNumber = bp.getLineNumber(); templateName = bp.getMarker().getAttribute( ILRDebugConstants.FM_TEMPLATE_NAME, "" ); } catch( CoreException e ) { PortalCore.logError( "Could not get breakpoint information.", e ); } } else { existingStepBp = thread.getStepBreakpoint(); currentLineNumber = existingStepBp.getLine(); templateName = existingStepBp.getTemplateName(); } if( currentLineNumber > -1 && templateName != null ) { final String remoteTemplateName = createRemoteTemplateName( templateName ); int stepLine = currentLineNumber + 1; Breakpoint existingBp = null; Debugger debugCli = getDebuggerClient(); try { List remoteBps = debugCli.getBreakpoints( remoteTemplateName ); for( Iterator i = remoteBps.iterator(); i.hasNext(); ) { Breakpoint remoteBp = (Breakpoint) i.next(); if( remoteBp.getLine() == stepLine ) { existingBp = remoteBp; break; } } if( existingBp == null ) { boolean addedRemote = false; while( ! addedRemote ) { Breakpoint newBp = new Breakpoint( remoteTemplateName, stepLine++ ); try { debugCli.addBreakpoint( newBp ); } catch( RemoteException e ) { // we except to get some remote exceptions if the next line is invalid breakpoint location } List updatedRemoteBps = debugCli.getBreakpoints( remoteTemplateName ); if( updatedRemoteBps.size() == remoteBps.size() + 1 ) // our new remote bp was sucessfully added { addedRemote = true; thread.setStepBreakpoint( newBp ); thread.setStepping( true ); fireResumeEvent( DebugEvent.RESUME ); if( existingStepBp != null) { debugCli.removeBreakpoint( existingStepBp ); } thread.getEnvironment().resume(); } } } else { // the next line already has a remote breakpoint installed so lets clear our "step" breakpoint thread.setStepBreakpoint( null ); thread.setStepping( false ); fireResumeEvent( DebugEvent.RESUME ); if( existingStepBp != null) { debugCli.removeBreakpoint( existingStepBp ); } thread.getEnvironment().resume(); } } catch( RemoteException e ) { PortalCore.logError( "Unable to check remote breakpoints", e ); } } else { PortalCore.logError( "Unable to step because of missing lineNumber or templateName information." ); } } public boolean supportsBreakpoint( IBreakpoint breakpoint ) { if( breakpoint.getModelIdentifier().equals( ILRDebugConstants.ID_FM_DEBUG_MODEL ) ) { try { return breakpoint.getMarker().getType().equals( PortalCore.ID_FM_BREAKPOINT_TYPE ); } catch( CoreException e ) { } } return false; } /* * (non-Javadoc) * @see org.eclipse.debug.core.model.IMemoryBlockRetrieval#supportsStorageRetrieval() */ public boolean supportsStorageRetrieval() { return false; } public void suspend() throws DebugException { } /** * Notification the target has suspended for the given reason * * @param detail * reason for the suspend */ private void suspended( int detail ) { this.suspended = true; this.fmThread.fireSuspendEvent( detail ); } boolean suspendRelatedJavaThread( final long remoteThreadId ) throws DebugException { boolean retval = false; for( IDebugTarget target : this.launch.getDebugTargets() ) { if( target instanceof IJavaDebugTarget ) { IJavaDebugTarget javaTarget = (IJavaDebugTarget) target; IThread[] threads = javaTarget.getThreads(); for( final IThread thread : threads ) { if( thread instanceof JDIThread ) { JDIThread jdiThread = (JDIThread) thread; ThreadReference underlyingThread = jdiThread.getUnderlyingThread(); Field tidField = underlyingThread.referenceType().fieldByName( "tid" ); Value tidValue = underlyingThread.getValue( tidField ); long threadId = Long.parseLong( tidValue.toString() ); if( threadId == remoteThreadId ) { thread.suspend(); break; } } } } } return retval; } public void terminate() throws DebugException { final IBreakpoint[] localBreakpoints = getEnabledLineBreakpoints(); removeRemoteBreakpoints( localBreakpoints ); resume(); terminated(); } /** * Called when this debug target terminates. */ private void terminated() { cleanup(); fireTerminateEvent(); } }