Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

allow evaluation of expressions in GHCi even if not suspended

  • Loading branch information...
commit c78cd471fee92814b00ec51d4e52b41755ccbafb 1 parent 7579025
@JPMoresmau authored
View
4 net.sf.eclipsefp.haskell-feature/feature.xml
@@ -81,7 +81,7 @@ available at http://www.eclipse.org/legal/epl-v10.html.
id="net.sf.eclipsefp.haskell.core"
download-size="0"
install-size="0"
- version="2.3.2"
+ version="2.3.3"
unpack="false"/>
<plugin
@@ -116,7 +116,7 @@ available at http://www.eclipse.org/legal/epl-v10.html.
id="net.sf.eclipsefp.haskell.debug.ui"
download-size="0"
install-size="0"
- version="2.3.2"
+ version="2.3.3"
unpack="false"/>
<plugin
View
4 net.sf.eclipsefp.haskell.core/META-INF/MANIFEST.MF
@@ -1,7 +1,7 @@
Manifest-Version: 1.0
Bundle-Name: %bundleName
Bundle-SymbolicName: net.sf.eclipsefp.haskell.core;singleton:=true
-Bundle-Version: 2.3.2
+Bundle-Version: 2.3.3
Bundle-Activator: net.sf.eclipsefp.haskell.core.HaskellCorePlugin
Bundle-Vendor: %bundleVendor
Bundle-Localization: plugin
@@ -11,7 +11,7 @@ Require-Bundle: org.eclipse.core.expressions;bundle-version="[3.2.0,4.0.0)",
org.eclipse.ui;bundle-version="3.5.0",
org.eclipse.text;bundle-version="3.5.0",
net.sf.eclipsefp.haskell.util;bundle-version="2.3.2",
- net.sf.eclipsefp.haskell.hlint;bundle-version="2.3.1",
+ net.sf.eclipsefp.haskell.hlint;bundle-version="2.3.2",
org.eclipse.ui.editors;bundle-version="3.5.0",
org.eclipse.ui.ide;bundle-version="3.5.2",
net.sf.eclipsefp.haskell.buildwrapper;bundle-version="2.3.0"
View
2  net.sf.eclipsefp.haskell.core/src/net/sf/eclipsefp/haskell/core/util/GHCiSyntax.java
@@ -46,6 +46,8 @@
public static final String TYPEOF="::"; //$NON-NLS-1$
public static final String UNIT="()"; //$NON-NLS-1$
+ public static final String IGNORING_BREAKPOINT="*** Ignoring breakpoint"+PlatformUtil.NL;//$NON-NLS-1$
+
public static String addModuleCommand(final String module){
return ":add *" +module; //$NON-NLS-1$
}
View
120 ...ipsefp.haskell.debug.core/src/net/sf/eclipsefp/haskell/debug/core/internal/debug/HaskellDebugTarget.java
@@ -9,10 +9,14 @@
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;
@@ -24,6 +28,7 @@
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;
@@ -80,7 +85,21 @@
private boolean disposed=false;
private boolean atEnd=false;
- private final HaskellThread thread=new HaskellThread( this );
+ 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
@@ -98,8 +117,25 @@ private static void instances(final int delta){
}
}
- public HaskellDebugTarget(final ILaunch launch, final IProcess process){
+ 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 );
@@ -138,13 +174,13 @@ public boolean supportsBreakpoint( final IBreakpoint breakpoint ) {
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 (launchProject!=null){
- if (launchProject.equals(marker.getResource().getProject())){
+ //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: launchProject.getReferencedProjects() ) {
+ for( IProject p: project.getReferencedProjects() ) {
if (p.equals(marker.getResource().getProject())){
return true;
}
@@ -399,11 +435,20 @@ public void start() throws DebugException{
}
}
+
+ /**
+ * @return the atEnd
+ */
+ public boolean isAtEnd() {
+ return atEnd;
+ }
+
//boolean runContext=true;
@Override
- public synchronized void streamAppended( final String text, final IStreamMonitor monitor ) {
+ public void streamAppended( final String text, final IStreamMonitor monitor ) {
//boolean needContext=false;
synchronized( response ) {
+ //boolean oldAtEnd=atEnd;
atEnd=false;
response.append(text);
/**
@@ -411,6 +456,7 @@ public synchronized void streamAppended( final String text, final IStreamMonitor
* 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()){
@@ -471,7 +517,11 @@ public void run() {
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();
@@ -501,7 +551,7 @@ public synchronized void getHistory() {
l.clear();
String line=br.readLine();
while (line!=null){
- HaskellStrackFrame f2=new HaskellStrackFrame( thread );
+ HaskellStrackFrame f2=new HaskellStrackFrame( thread,project );
f2.setHistoryLocation( line );
if (f2.getName()!=null){
l.add( f2 );
@@ -534,7 +584,10 @@ public synchronized void getHistory() {
while (line!=null){
if (line.indexOf( GHCiSyntax.TYPEOF )>-1 && sb.length()>0){
- ret.add( new HaskellVariable( sb.toString(), frame ) );
+ HaskellVariable var=new HaskellVariable( sb.toString(), frame );
+ if (!myVars.contains( var.getName() )){
+ ret.add( var );
+ }
sb.setLength( 0 );
}
if (sb.length()>0){
@@ -544,7 +597,10 @@ public synchronized void getHistory() {
line=br.readLine();
}
if (sb.length()>0){
- ret.add( new HaskellVariable( sb.toString(), frame ) );
+ 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){
@@ -555,7 +611,8 @@ public synchronized void getHistory() {
public void forceVariable(final HaskellVariable var)throws DebugException{
- sendRequest( GHCiSyntax.forceVariableCommand( var.getName() ), true );
+ //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();
@@ -575,16 +632,47 @@ public void forceVariable(final HaskellVariable var)throws DebugException{
}
/**
+ * 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)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);
- sendRequest(expression,true);
- String val=getResultWithoutPrompt();
+ String val=sendExpression( expression, force );
+ //getResultWithoutPrompt();
sendRequest(GHCiSyntax.TYPE_LAST_RESULT_COMMAND,true);
String type=getResultWithoutPrompt();
int ix=type.indexOf( GHCiSyntax.TYPEOF );
View
28 ...ipsefp.haskell.debug.core/src/net/sf/eclipsefp/haskell/debug/core/internal/debug/HaskellStrackFrame.java
@@ -2,10 +2,8 @@
import java.util.regex.Matcher;
import net.sf.eclipsefp.haskell.core.util.GHCiSyntax;
-import net.sf.eclipsefp.haskell.debug.core.internal.launch.ILaunchAttributes;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
-import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
@@ -40,9 +38,12 @@
private boolean hasVariables=false;
- public HaskellStrackFrame(final HaskellThread thread){
+ private final IProject project;
+
+ public HaskellStrackFrame(final HaskellThread thread,final IProject p){
super(thread.getDebugTarget());
this.thread=thread;
+ this.project=p;
}
@Override
@@ -139,7 +140,7 @@ public void setLocation( final String location ) {
DebugPlugin.getDefault().fireDebugEventSet(new DebugEvent[]{new DebugEvent( this, DebugEvent.CHANGE, DebugEvent.CONTENT )});
}
- private String removeFilePrefix(final String fullLocation) throws CoreException{
+ private String removeFilePrefix(final String fullLocation) {
String ret=fullLocation;
if (fullLocation!=null && fullLocation.length()>0){
IProject p=getProject();
@@ -263,13 +264,13 @@ public String getFileName() throws CoreException{
return fileName;
}
- public IProject getProject() throws CoreException {
- String projectName=getLaunch().getLaunchConfiguration().getAttribute( ILaunchAttributes.PROJECT_NAME ,(String)null);
+ public IProject getProject() {
+ /*String projectName=getLaunch().getLaunchConfiguration().getAttribute( ILaunchAttributes.PROJECT_NAME ,(String)null);
if (projectName!=null){
IProject p=ResourcesPlugin.getWorkspace().getRoot().getProject( projectName );
return p;
- }
- return null;
+ }*/
+ return project;
}
@Override
@@ -371,4 +372,15 @@ public void terminate() throws DebugException {
}
+
+ public void setName( final String name ) {
+ this.name = name;
+ }
+
+
+ public void setUnprocessedFileName( final String unprocessedFileName ) {
+ this.unprocessedFileName = unprocessedFileName;
+ this.name=removeFilePrefix( unprocessedFileName );
+ }
+
}
View
14 ...f.eclipsefp.haskell.debug.core/src/net/sf/eclipsefp/haskell/debug/core/internal/debug/HaskellThread.java
@@ -4,6 +4,7 @@
import java.util.List;
import net.sf.eclipsefp.haskell.core.util.GHCiSyntax;
import net.sf.eclipsefp.haskell.debug.core.internal.util.CoreTexts;
+import org.eclipse.core.resources.IProject;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
@@ -19,13 +20,22 @@
public class HaskellThread extends HaskellDebugElement implements IThread {
private HaskellBreakpoint breakpoint;
private String stopLocation;
- private final HaskellStrackFrame frame=new HaskellStrackFrame( this );
+ private final HaskellStrackFrame frame;
private String name=CoreTexts.thread_default_name;
private final List<HaskellStrackFrame> historyFrames=new ArrayList<HaskellStrackFrame>();
- public HaskellThread(final HaskellDebugTarget target){
+ public HaskellThread(final HaskellDebugTarget target,final IProject p){
super( target );
+ frame=new HaskellStrackFrame( this,p );
+ }
+
+
+ /**
+ * @return the frame
+ */
+ public HaskellStrackFrame getDefaultFrame() {
+ return frame;
}
@Override
View
7 ...fp.haskell.debug.core/src/net/sf/eclipsefp/haskell/debug/core/internal/launch/HaskellLaunchDelegate.java
@@ -4,6 +4,8 @@
package net.sf.eclipsefp.haskell.debug.core.internal.launch;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import net.sf.eclipsefp.haskell.debug.core.internal.HaskellDebugCore;
import net.sf.eclipsefp.haskell.debug.core.internal.debug.HaskellDebugTarget;
@@ -45,8 +47,11 @@ protected void postProcessCreation( final ILaunchConfiguration configuration,
process.setAttribute( HaskellDebugCore.PROCESS_COMMAND_HISTORY, Boolean.TRUE.toString() );
+ List<String> fileNames=configuration.getAttribute( ILaunchAttributes.FILES, new ArrayList<String>() );
+
if (mode.equals( ILaunchManager.DEBUG_MODE )){
- HaskellDebugTarget hdt=new HaskellDebugTarget( launch, process );
+ HaskellDebugTarget hdt=new HaskellDebugTarget( launch, process,fileNames );
+
launch.addDebugTarget(hdt);
hdt.start();
}
View
2  net.sf.eclipsefp.haskell.debug.ui/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %bundleName
Bundle-SymbolicName: net.sf.eclipsefp.haskell.debug.ui;singleton:=true
-Bundle-Version: 2.3.2
+Bundle-Version: 2.3.3
Bundle-Activator: net.sf.eclipsefp.haskell.debug.ui.internal.HaskellDebugUI
Bundle-Vendor: %bundleVendor
Require-Bundle: net.sf.eclipsefp.haskell.ghccompiler,
View
189 ...askell.debug.ui/src/net/sf/eclipsefp/haskell/debug/ui/internal/debug/HaskellWatchExpressionDelegate.java
@@ -1,91 +1,98 @@
-package net.sf.eclipsefp.haskell.debug.ui.internal.debug;
-
-import net.sf.eclipsefp.haskell.debug.core.internal.debug.HaskellDebugElement;
-import net.sf.eclipsefp.haskell.debug.core.internal.debug.HaskellValue;
-import net.sf.eclipsefp.haskell.debug.ui.internal.util.UITexts;
-import org.eclipse.debug.core.DebugException;
-import org.eclipse.debug.core.model.IDebugElement;
-import org.eclipse.debug.core.model.ISuspendResume;
-import org.eclipse.debug.core.model.IValue;
-import org.eclipse.debug.core.model.IWatchExpressionDelegate;
-import org.eclipse.debug.core.model.IWatchExpressionListener;
-import org.eclipse.debug.core.model.IWatchExpressionResult;
-
-/**
- * Delegate to evaluate haskell expression in GHCi debugging session
- * @author JP Moresmau
- *
- */
-public class HaskellWatchExpressionDelegate implements IWatchExpressionDelegate {
-
- @Override
- public void evaluateExpression( final String expression, final IDebugElement context,
- final IWatchExpressionListener listener ) {
- HaskellDebugElement hde=(HaskellDebugElement)context;
- if ((hde instanceof ISuspendResume && ((ISuspendResume)hde).isSuspended()) || hde.getDebugTarget().isSuspended()){
- try {
- HaskellValue val=hde.getDebugTarget().evaluate( expression );
- listener.watchEvaluationFinished( new WatchExpressionResult( expression, val, null ));
-
- } catch (DebugException de){
- listener.watchEvaluationFinished( new WatchExpressionResult( expression, null, de ));
-
- }
- } else {
- listener.watchEvaluationFinished( new WatchExpressionResult(expression,null,null) );
- }
-
- }
-
- /**
- * Wraps either:
- * - a HaskellValue if we could evaluate the expression
- * - A DebugException if there was an error
- * - Nothing if we couldn't evaluate at all because we weren't suspended
- * @author jean-philippem
- *
- */
- private class WatchExpressionResult implements IWatchExpressionResult{
- private final HaskellValue val;
- private final DebugException exception;
- private final String expression;
-
-
-
- public WatchExpressionResult( final String expression, final HaskellValue val,
- final DebugException exception ) {
- this.expression = expression;
- this.val = val;
- this.exception = exception;
- }
-
- @Override
- public boolean hasErrors() {
- return exception!=null;
- }
-
- @Override
- public IValue getValue() {
- return val;
- }
-
- @Override
- public String getExpressionText() {
- return expression;
- }
-
- @Override
- public DebugException getException() {
- return exception;
- }
-
- @Override
- public String[] getErrorMessages() {
- if (val==null && exception==null){
- return new String[]{UITexts.evaluate_need_suspend};
- }
- return new String[0];
- }
- }
-
-}
+package net.sf.eclipsefp.haskell.debug.ui.internal.debug;
+
+import net.sf.eclipsefp.haskell.debug.core.internal.debug.HaskellDebugElement;
+import net.sf.eclipsefp.haskell.debug.core.internal.debug.HaskellValue;
+import net.sf.eclipsefp.haskell.debug.ui.internal.util.UITexts;
+import org.eclipse.debug.core.DebugException;
+import org.eclipse.debug.core.model.IDebugElement;
+import org.eclipse.debug.core.model.ISuspendResume;
+import org.eclipse.debug.core.model.IValue;
+import org.eclipse.debug.core.model.IWatchExpressionDelegate;
+import org.eclipse.debug.core.model.IWatchExpressionListener;
+import org.eclipse.debug.core.model.IWatchExpressionResult;
+
+/**
+ * Delegate to evaluate haskell expression in GHCi debugging session
+ * @author JP Moresmau
+ *
+ */
+public class HaskellWatchExpressionDelegate implements IWatchExpressionDelegate {
+
+ @Override
+ public void evaluateExpression( final String expression, final IDebugElement context,
+ final IWatchExpressionListener listener ) {
+ HaskellDebugElement hde=(HaskellDebugElement)context;
+ /** this code allows expressions that are valid in the scope of the module to be evaluated
+ * however, it gets problematic when we also have breakpoints: the top level expression may also reach the breakpoint
+ * so when we reach the breakpoint once, we stop, try to reevaluate the top level expression which in turn reaches the breakpoint...
+ * the solution is to always :force the evals if we're not considered suspended
+ */
+ boolean isSuspended=(hde instanceof ISuspendResume && ((ISuspendResume)hde).isSuspended()) || hde.getDebugTarget().isSuspended();
+ if (isSuspended|| hde.getDebugTarget().isAtEnd()){
+ try {
+ boolean force=!isSuspended;
+ HaskellValue val=hde.getDebugTarget().evaluate( expression,force );
+ listener.watchEvaluationFinished( new WatchExpressionResult( expression, val, null ));
+
+ } catch (DebugException de){
+ listener.watchEvaluationFinished( new WatchExpressionResult( expression, null, de ));
+
+ }
+ } else {
+ listener.watchEvaluationFinished( new WatchExpressionResult(expression,null,null) );
+ }
+
+ }
+
+ /**
+ * Wraps either:
+ * - a HaskellValue if we could evaluate the expression
+ * - A DebugException if there was an error
+ * - Nothing if we couldn't evaluate at all because we weren't suspended
+ * @author jean-philippem
+ *
+ */
+ private class WatchExpressionResult implements IWatchExpressionResult{
+ private final HaskellValue val;
+ private final DebugException exception;
+ private final String expression;
+
+
+
+ public WatchExpressionResult( final String expression, final HaskellValue val,
+ final DebugException exception ) {
+ this.expression = expression;
+ this.val = val;
+ this.exception = exception;
+ }
+
+ @Override
+ public boolean hasErrors() {
+ return exception!=null;
+ }
+
+ @Override
+ public IValue getValue() {
+ return val;
+ }
+
+ @Override
+ public String getExpressionText() {
+ return expression;
+ }
+
+ @Override
+ public DebugException getException() {
+ return exception;
+ }
+
+ @Override
+ public String[] getErrorMessages() {
+ if (val==null && exception==null){
+ return new String[]{UITexts.evaluate_need_suspend};
+ }
+ return new String[0];
+ }
+ }
+
+}
View
2  net.sf.eclipsefp.haskell.ui/src/net/sf/eclipsefp/haskell/ui/handlers/DisplayExpressionHandler.java
@@ -40,7 +40,7 @@ protected void addExpression(final HaskellEditor hEditor, final String s ) {
final HaskellDebugElement hde=(HaskellDebugElement)context;
if ((hde instanceof ISuspendResume && ((ISuspendResume)hde).isSuspended()) || hde.getDebugTarget().isSuspended()){
try {
- final HaskellValue val=hde.getDebugTarget().evaluate( s );
+ final HaskellValue val=hde.getDebugTarget().evaluate( s,true );
hEditor.getEditorSite().getShell().getDisplay().asyncExec( new Runnable(){
@Override
public void run() {
Please sign in to comment.
Something went wrong with that request. Please try again.