/**
* Copyright (c) 2012 by JP Moresmau
* This code is made available under the terms of the Eclipse Public License,
* version 1.0 (EPL). See http://www.eclipse.org/legal/epl-v10.html
*/
package net.sf.eclipsefp.haskell.debug.core.internal.debug;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import net.sf.eclipsefp.haskell.core.HaskellCorePlugin;
import net.sf.eclipsefp.haskell.core.preferences.ICorePreferenceNames;
import net.sf.eclipsefp.haskell.core.util.GHCiSyntax;
import net.sf.eclipsefp.haskell.core.util.ResourceUtil;
import net.sf.eclipsefp.haskell.debug.core.internal.HaskellDebugCore;
import net.sf.eclipsefp.haskell.debug.core.internal.launch.ILaunchAttributes;
import net.sf.eclipsefp.haskell.util.PlatformUtil;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.IStreamListener;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IStreamMonitor;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.debug.core.model.IVariable;
/**
* debug target for haskell interactive launch
* @author JP Moresmau
*
*/
public class HaskellDebugTarget extends HaskellDebugElement implements IDebugTarget,IStreamListener {
/**
* lock
*/
private static Object instanceLock=new Object();
/**
* number of target currently suspended at a breakpoint
*/
private static int instances=0;
/**
* system property to write globally if we can handle inspect
*/
private static final String SYSTEM_PROPERTY="haskell.debug"; //$NON-NLS-1$
// associated system process (VM)
private final IProcess fProcess;
// containing launch object
private final ILaunch fLaunch;
// program name
private String fName;
private final StringBuilder response=new StringBuilder();
private final Map<IBreakpoint,Integer> breakpointIds=new IdentityHashMap<>();
private final Map<String,HaskellBreakpoint> breakpointNames=new HashMap<>();
private boolean connected=true;
private boolean disposed=false;
private boolean atEnd=false;
private final HaskellThread thread;
/**
* the project on which we've laucnhed the debug session
*/
private final IProject project;
/**
* keep a unique ID for the variables we create for :force
*/
private final AtomicLong expCounter=new AtomicLong(System.currentTimeMillis());
/**
* keep all the :forced variables so that we don't show them in the variables view
*/
private final Set<String> myVars=Collections.synchronizedSet( new HashSet<String>());
/**
* manages the number of instances
* @param delta the number to move the count by
*/
private static void instances(final int delta){
synchronized( instanceLock ) {
instances+=delta;
if(instances>0){
System.setProperty( SYSTEM_PROPERTY, "true" ); //$NON-NLS-1$
} else {
instances=0;
System.clearProperty( SYSTEM_PROPERTY );
}
}
}
public HaskellDebugTarget(final ILaunch launch, final IProcess process,final List<String> files) throws CoreException{
setTarget( this );
// try {
String projectName=launch.getLaunchConfiguration().getAttribute( ILaunchAttributes.PROJECT_NAME ,(String)null);
if (projectName!=null){
project=ResourcesPlugin.getWorkspace().getRoot().getProject( projectName );
} else {
project=null;
}
thread=new HaskellThread( this, project);
if (files.size()>0){
String fn=files.get( 0 );
thread.getDefaultFrame().setUnprocessedFileName( fn );
}
// } catch (CoreException ce){
// HaskellDebugCore.log( ce.getLocalizedMessage(), ce );
// }
this.fLaunch=launch;
this.fProcess=process;
this.fProcess.getStreamsProxy().getOutputStreamMonitor().addListener( this );
DebugPlugin.getDefault().getBreakpointManager().addBreakpointListener(this);
}
@Override
public String getName(){
if (fName == null) {
fName = getLaunch().getLaunchConfiguration().getName();
}
return fName;
}
@Override
public IProcess getProcess() {
return fProcess;
}
@Override
public IThread[] getThreads(){
return new IThread[]{thread};
}
@Override
public boolean hasThreads() {
return true;
}
@Override
public boolean supportsBreakpoint( final IBreakpoint breakpoint ) {
if (breakpoint.getModelIdentifier().equals(HaskellDebugCore.ID_HASKELL_DEBUG_MODEL)) {
// see http://sourceforge.net/projects/eclipsefp/forums/forum/371922/topic/5091430, we don't want to reduce only to our project
IMarker marker = breakpoint.getMarker();
if (marker != null) {
try {
//String project = getLaunch().getLaunchConfiguration().getAttribute(ILaunchAttributes.PROJECT_NAME, (String)null);
//IProject launchProject=marker.getResource().getProject().getWorkspace().getRoot().getProject( project );
if (project!=null){
if (project.equals(marker.getResource().getProject())){
return true;
}
for( IProject p: project.getReferencedProjects() ) {
if (p.equals(marker.getResource().getProject())){
return true;
}
}
}
} catch (CoreException e) {
HaskellCorePlugin.log( e );
}
}
// try {
// String project = getLaunch().getLaunchConfiguration().getAttribute(ILaunchAttributes.PROJECT_NAME, (String)null);
// if (project != null) {
// IMarker marker = breakpoint.getMarker();
// if (marker != null) {
// return project.equals(marker.getResource().getProject().getName());
// }
// }
// } catch (CoreException e) {
// HaskellCorePlugin.log( e );
// }
}
return false;
}
@Override
public ILaunch getLaunch() {
return fLaunch;
}
@Override
public boolean canTerminate() {
return !disposed && (!connected || fProcess.canTerminate());
}
@Override
public boolean isTerminated() {
boolean t=fProcess.isTerminated() || disposed;
if (t && !disposed){
dispose();
}
return t;
}
protected synchronized void sendRequest(final String command,final boolean wait)throws DebugException{
synchronized( response ) {
response.setLength( 0 );
atEnd=false;
}
try {
if (fProcess!=null && !fProcess.isTerminated()){
fProcess.getStreamsProxy().write(command);
fProcess.getStreamsProxy().write(PlatformUtil.NL);
if (wait){
waitForPrompt();
}
} else {
dispose();
}
} catch (IOException ioe){
throw new DebugException(new Status(IStatus.ERROR,HaskellDebugCore.getPluginId(),ioe.getLocalizedMessage(),ioe));
}
}
private synchronized void waitForPrompt(){
long timeout=10; // seconds
long t0=System.currentTimeMillis();
try {
while(!atEnd && System.currentTimeMillis()-t0<(timeout * 1000)){
wait(100);
}
} catch (InterruptedException ie){
ie.printStackTrace();
}
}
@Override
public void terminate() throws DebugException {
if (isSuspended()){
instances(-1);
thread.setBreakpoint( null );
thread.setStopLocation( null );
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( thread, DebugEvent.RESUME )});
}
// if disconnected, leave GHCi running
if (connected){
sendRequest( GHCiSyntax.QUIT_COMMAND,false );
}
dispose();
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( this, DebugEvent.TERMINATE ),new DebugEvent( thread, DebugEvent.TERMINATE )});
}
@Override
public boolean canResume() {
return connected && isSuspended();
}
@Override
public boolean canSuspend() {
return false;
}
@Override
public boolean isSuspended() {
return thread.getBreakpoints().length>0 || thread.getStopLocation()!=null;
}
@Override
public void resume() throws DebugException {
thread.setBreakpoint( null );
thread.setStopLocation( null );
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( thread, DebugEvent.RESUME )});
sendRequest( GHCiSyntax.CONTINUE_COMMAND, false );
instances(-1);
}
@Override
public void suspend() {
// NOOP
}
@Override
public synchronized void breakpointAdded( final IBreakpoint breakpoint ) {
if (supportsBreakpoint(breakpoint)) {
try {
if (breakpoint.isEnabled()) {
HaskellBreakpoint hb=(HaskellBreakpoint)breakpoint;
// TODO take out GHCi specific
IPath path=ResourceUtil.getSourceFolderRelativeName( hb.getMarker().getResource() );
String module=ResourceUtil.getModuleName( path.toPortableString() );
sendRequest(GHCiSyntax.setBreakpointCommand(module.replace('/','.'),(hb.getLineNumber()) ),true);
String s=response.toString();
/*int ix=s.indexOf( "Breakpoint " );
ix+="Breakpoint ".length();
int ix2=s.indexOf( " activated ",ix );
Integer id=Integer.valueOf( s.substring( ix,ix2 ) );
int ix3=s.indexOf( ResourceUtil.NL,ix2 );*/
Matcher m=GHCiSyntax.BREAKPOINT_SET_PATTERN.matcher( s );
boolean found=m.find();
if (!found){
try {
wait(100);
} catch (InterruptedException ie){
ie.printStackTrace();
}
s=response.toString();
m=GHCiSyntax.BREAKPOINT_SET_PATTERN.matcher( s );
found=m.find();
}
if (found){
Integer id=Integer.valueOf(m.group( 1 ));
String name=m.group( 2 );
breakpointIds.put( breakpoint, id );
breakpointNames.put(name,hb);
} else {
System.err.println(s);
}
}
} catch (CoreException e) {
HaskellCorePlugin.log( e );
}
}
}
@Override
public void breakpointChanged( final IBreakpoint breakpoint, final IMarkerDelta delta ) {
if (supportsBreakpoint(breakpoint)) {
try {
if (breakpoint.isEnabled()) {
breakpointAdded(breakpoint);
} else {
breakpointRemoved(breakpoint, null);
}
} catch (CoreException e) {
HaskellCorePlugin.log( e );
}
}
}
@Override
public void breakpointRemoved( final IBreakpoint breakpoint, final IMarkerDelta delta ) {
// TODO take out GHCi specific
Integer id=breakpointIds.get(breakpoint);
if (id!=null){
try {
sendRequest(GHCiSyntax.deleteBreakpointCommand( id.intValue()),true);
} catch (CoreException e) {
HaskellCorePlugin.log( e );
}
}
}
@Override
public boolean canDisconnect() {
return connected;
}
@Override
public void disconnect() throws DebugException {
try {
// TODO take out GHCi specific
sendRequest(GHCiSyntax.DELETE_ALL_BREAKPOINTS_COMMAND,true);
} catch (CoreException e) {
throw new DebugException(new Status(IStatus.ERROR,HaskellDebugCore.getPluginId(),e.getLocalizedMessage(),e));
}
dispose();
}
public void dispose(){
connected=false;
disposed=true;
DebugPlugin.getDefault().getBreakpointManager().removeBreakpointListener( this );
}
@Override
public boolean isDisconnected() {
return !connected;
}
@Override
public IMemoryBlock getMemoryBlock( final long startAddress, final long length ) {
return null;
}
@Override
public boolean supportsStorageRetrieval() {
return false;
}
public void start() throws DebugException{
//waitForPrompt();
IBreakpoint[] breakpoints = DebugPlugin.getDefault().getBreakpointManager().getBreakpoints(HaskellDebugCore.ID_HASKELL_DEBUG_MODEL);
for (int i = 0; i < breakpoints.length; i++) {
breakpointAdded(breakpoints[i]);
}
IPreferencesService service = Platform.getPreferencesService();
if (service.getBoolean(HaskellCorePlugin.getPluginId(), ICorePreferenceNames.DEBUG_PRINT_WITH_SHOW ,true,null)){
sendRequest( GHCiSyntax.SET_PRINT_WITH_SHOW_COMMAND, false );
}
if (service.getBoolean(HaskellCorePlugin.getPluginId(), ICorePreferenceNames.DEBUG_BREAK_ON_ERROR,false,null )){
sendRequest( GHCiSyntax.SET_BREAK_ON_ERROR_COMMAND, false );
}
if (service.getBoolean(HaskellCorePlugin.getPluginId(), ICorePreferenceNames.DEBUG_BREAK_ON_EXCEPTION,false,null )){
sendRequest( GHCiSyntax.SET_BREAK_ON_EXCEPTION_COMMAND, false );
}
}
/**
* @return the atEnd
*/
public boolean isAtEnd() {
return atEnd;
}
//boolean runContext=true;
@Override
public void streamAppended( final String text, final IStreamMonitor monitor ) {
//boolean needContext=false;
synchronized( response ) {
//boolean oldAtEnd=atEnd;
atEnd=false;
response.append(text);
/**
* what do we do here? We got users complaining that sometimes we didn't realize GHCi was at the prompt
* the only explanation I could find is that the PROMPT_END string we look for got actually cut in two
* and so the text parameter never contained it. So if text is smaller than PROMPT_END, we check the whole response
*/
atEnd=text.length()>=GHCiSyntax.PROMPT_END.length()?text.endsWith( GHCiSyntax.PROMPT_END):response.toString().endsWith( GHCiSyntax.PROMPT_END);
if (atEnd){
if (thread.isSuspended()){
Matcher m2=GHCiSyntax.CONTEXT_PATTERN.matcher( response.toString() );
if (m2.find()){
String name=m2.group( 1 );
thread.setName( name );
response.setLength( 0 );
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( thread, DebugEvent.CHANGE,DebugEvent.STATE )});
//notify();
Runnable r=new Runnable(){
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
getHistory();
}
};
new Thread(r).start();
return;
}
}
Matcher m=GHCiSyntax.BREAKPOINT_STOP_PATTERN.matcher( response.toString() );
if (m.find()){
String location=m.group( 1 );
HaskellBreakpoint hb=breakpointNames.get(location );
boolean wasSuspended=thread.isSuspended();
if (hb!=null){
thread.setBreakpoint( hb );
} else {
thread.setStopLocation( location );
}
response.setLength( 0 );
if (thread.isSuspended()){
HaskellStrackFrame hsf=(HaskellStrackFrame)thread.getTopStackFrame();
hsf.setLocation( location );
//needContext=true;
if (!wasSuspended){
try {
//runContext=false;
sendRequest( GHCiSyntax.SHOW_CONTEXT_COMMAND, false );
} catch (DebugException de){
HaskellCorePlugin.log( de );
}
}
}
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( thread, DebugEvent.SUSPEND,hb!=null?DebugEvent.BREAKPOINT:DebugEvent.UNSPECIFIED )});
instances(1);
} else {
m=GHCiSyntax.BREAKPOINT_NOT.matcher( response.toString() );
if (m.find()){
thread.setBreakpoint(null);
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( thread, DebugEvent.RESUME )});
response.setLength( 0 );
} /*else if (!oldAtEnd){
response.setLength( 0 );
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( thread, DebugEvent.SUSPEND,DebugEvent.UNSPECIFIED )});
instances(1);
}*/
}
}
//notify();
}
/*if (needContext) {
try {
//runContext=false;
sendRequest( GHCiSyntax.SHOW_CONTEXT_COMMAND, false );
} catch (DebugException de){
HaskellCorePlugin.log( de );
} finally {
//runContext=true;
}
}*/
}
public synchronized void getHistory() {
try {
sendRequest( GHCiSyntax.HIST_COMMAND, true );
String s=getResultWithoutPrompt();
BufferedReader br=new BufferedReader(new StringReader( s ));
try {
List<HaskellStrackFrame> l=thread.getHistoryFrames();
synchronized( l ) {
l.clear();
String line=br.readLine();
while (line!=null){
HaskellStrackFrame f2=new HaskellStrackFrame( thread,project );
f2.setHistoryLocation( line );
if (f2.getName()!=null){
l.add( f2 );
}
line=br.readLine();
}
}
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( thread, DebugEvent.CHANGE,DebugEvent.STATE )});
//notify();
} catch (IOException ioe){
HaskellCorePlugin.log( ioe );
}
} catch (DebugException de){
HaskellCorePlugin.log( de );
}
}
public synchronized IVariable[] getVariables( final HaskellStrackFrame frame ) throws DebugException {
if (!frame.hasVariables()){
return new IVariable[0];
}
sendRequest( GHCiSyntax.SHOW_BINDINGS_COMMAND, true );
String s=getResultWithoutPrompt();
BufferedReader br=new BufferedReader(new StringReader( s ));
try {
List<IVariable> ret=new ArrayList<>();
String line=br.readLine();
StringBuilder sb=new StringBuilder();
while (line!=null){
if (line.indexOf( GHCiSyntax.TYPEOF )>-1 && sb.length()>0){
HaskellVariable var=new HaskellVariable( sb.toString(), frame );
if (!myVars.contains( var.getName() )){
ret.add( var );
}
sb.setLength( 0 );
}
if (sb.length()>0){
sb.append(PlatformUtil.NL);
}
sb.append( line );
line=br.readLine();
}
if (sb.length()>0){
HaskellVariable var=new HaskellVariable( sb.toString(), frame );
if (!myVars.contains( var.getName() )){
ret.add( var );
}
}
return ret.toArray( new IVariable[ret.size()] );
} catch (IOException ioe){
throw new DebugException(new Status(IStatus.ERROR,HaskellDebugCore.getPluginId(),ioe.getLocalizedMessage(),ioe));
}
}
public void forceVariable(final HaskellVariable var)throws DebugException{
//sendRequest( GHCiSyntax.forceVariableCommand( var.getName() ), true );
sendExpression( var.getName(), true );
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( var.getFrame(), DebugEvent.CHANGE,DebugEvent.CONTENT)});
/*String s=response.toString();
BufferedReader br=new BufferedReader(new StringReader( s ));
try {
String line=br.readLine();
while (line!=null){
if (line.indexOf( GHCiSyntax.TYPEOF )>-1){
return line;
}
line=br.readLine();
}
return null;
} catch (IOException ioe){
throw new DebugException(new Status(IStatus.ERROR,HaskellDebugCore.getPluginId(),ioe.getLocalizedMessage(),ioe));
}*/
}
/**
* since :force only takes an identifier, we need to create an identifier for the expression, and force that
* @param expression
* @param force
* @return
* @throws DebugException
*/
private String sendExpression(final String expression,final boolean force) throws DebugException{
if (force){
String fp="fp"+expCounter.getAndIncrement(); //$NON-NLS-1$
myVars.add( fp );
String exp=force?"let "+fp+"="+expression:expression; //$NON-NLS-1$ //$NON-NLS-2$
//GHCiSyntax.forceVariableCommand(expression):expression;
sendRequest(exp,true);
String val1=getResultWithoutPrompt();
sendRequest( GHCiSyntax.forceVariableCommand(fp), true );
String val2=getResultWithoutPrompt();
if (val2.startsWith( GHCiSyntax.IGNORING_BREAKPOINT )){ /** :force may give this message **/
val2=val2.substring( GHCiSyntax.IGNORING_BREAKPOINT.length() );
}
if (val2.startsWith( fp +" = ") ){//$NON-NLS-1$
val2=val2.substring( fp.length()+3 );
} else { /** this is an error, then, so we give the result of the initial expression **/
val2=val1;
}
return val2;
}
sendRequest(expression,true);
return getResultWithoutPrompt();
}
/**
* evaluate an arbitrary expression
* @param expression the expression
* @return the value and its type
* @throws DebugException
*/
public synchronized HaskellValue evaluate(final String expression,final boolean force)throws DebugException{
// get rid of any previous "it" in case of evaluation error
sendRequest(GHCiSyntax.UNIT,true);
String val=sendExpression( expression, force );
//getResultWithoutPrompt();
sendRequest(GHCiSyntax.TYPE_LAST_RESULT_COMMAND,true);
String type=getResultWithoutPrompt();
int ix=type.indexOf( GHCiSyntax.TYPEOF );
if (ix>-1){
type=type.substring( ix+GHCiSyntax.TYPEOF.length() ).trim();
} else {
type=""; //$NON-NLS-1$
}
return new HaskellValue(this,type,val);
}
private String getResultWithoutPrompt(){
String s=response.toString();
int ix=s.lastIndexOf( PlatformUtil.NL );
if(ix>-1){
s=s.substring(0,ix).trim();
}
return s;
}
}