blob: 22b7625f9ab46ebe77df854fae9bef03d40a7ee5 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017 Kichwa Coders Ltd. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.lsp4e.debug.debugmodel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.core.runtime.Assert;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.lsp4e.debug.DSPPlugin;
import org.eclipse.lsp4j.debug.ContinueArguments;
import org.eclipse.lsp4j.debug.NextArguments;
import org.eclipse.lsp4j.debug.PauseArguments;
import org.eclipse.lsp4j.debug.StackFrame;
import org.eclipse.lsp4j.debug.StackTraceArguments;
import org.eclipse.lsp4j.debug.StepInArguments;
import org.eclipse.lsp4j.debug.StepOutArguments;
import org.eclipse.lsp4j.debug.Thread;
public class DSPThread extends DSPDebugElement implements IThread {
private final Integer id;
/**
* The name may not be known, if it is requested we will ask for it from the
* target.
*/
private String name;
private List<DSPStackFrame> frames = Collections.synchronizedList(new ArrayList<>());
private AtomicBoolean refreshFrames = new AtomicBoolean(true);
private boolean stepping;
private boolean isSuspended = false;
public DSPThread(DSPDebugTarget debugTarget, Thread thread) {
super(debugTarget);
this.id = thread.getId();
this.name = thread.getName();
}
public DSPThread(DSPDebugTarget debugTarget, Integer threadId) {
super(debugTarget);
this.id = threadId;
}
/**
* Update properties of the thread. The ID can't be changed, so the passed in
* thread should match.
*
* @param thread
* @throws DebugException
*/
public void update(Thread thread) {
Assert.isTrue(Objects.equals(this.id, thread.getId()));
this.name = thread.getName();
refreshFrames.set(true);
// fireChangeEvent(DebugEvent.STATE);
}
@Override
public void terminate() throws DebugException {
getDebugTarget().terminate();
}
@Override
public boolean isTerminated() {
return getDebugTarget().isTerminated();
}
@Override
public boolean canTerminate() {
return getDebugTarget().canTerminate();
}
private <T> T handleExceptionalResume(Throwable t) {
DSPPlugin.logError("Failed to resume debug adapter", t);
setErrorMessage(t.getMessage());
stopped();
fireSuspendEvent(DebugEvent.CLIENT_REQUEST);
return null;
}
public void continued() {
isSuspended = false;
refreshFrames.set(true);
}
public void stopped() {
isSuspended = true;
stepping = false;
refreshFrames.set(true);
}
@Override
public void stepReturn() throws DebugException {
continued();
stepping = true;
fireResumeEvent(DebugEvent.STEP_OVER);
StepOutArguments arguments = new StepOutArguments();
arguments.setThreadId(id);
getDebugProtocolServer().stepOut(arguments).exceptionally(this::handleExceptionalResume);
}
@Override
public void stepOver() throws DebugException {
continued();
stepping = true;
fireResumeEvent(DebugEvent.STEP_OVER);
NextArguments arguments = new NextArguments();
arguments.setThreadId(id);
getDebugProtocolServer().next(arguments).exceptionally(this::handleExceptionalResume);
}
@Override
public void stepInto() throws DebugException {
continued();
stepping = true;
fireResumeEvent(DebugEvent.STEP_INTO);
StepInArguments arguments = new StepInArguments();
arguments.setThreadId(id);
getDebugProtocolServer().stepIn(arguments).exceptionally(this::handleExceptionalResume);
}
@Override
public void resume() throws DebugException {
continued();
stepping = true;
fireResumeEvent(DebugEvent.RESUME);
ContinueArguments arguments = new ContinueArguments();
arguments.setThreadId(id);
getDebugProtocolServer().continue_(arguments).thenAccept(response -> {
if (response == null || response.getAllThreadsContinued() == null || response.getAllThreadsContinued()) {
// turns out everything was resumed, so need to update other threads too
getDebugTarget().fireResumeEvent(DebugEvent.CLIENT_REQUEST);
}
}).exceptionally(this::handleExceptionalResume);
}
@Override
public boolean isStepping() {
return stepping;
}
private boolean canResumeOrStep() {
return isSuspended();
// TODO add additional restrictions, like if an evaluation is happening?
}
@Override
public boolean canStepReturn() {
return canResumeOrStep();
}
@Override
public boolean canStepOver() {
return canResumeOrStep();
}
@Override
public boolean canStepInto() {
return canResumeOrStep();
}
@Override
public boolean canResume() {
return canResumeOrStep();
}
@Override
public void suspend() throws DebugException {
PauseArguments arguments = new PauseArguments();
arguments.setThreadId(id);
getDebugProtocolServer().pause(arguments).exceptionally(t -> {
DSPPlugin.logError("Failed to suspend debug adapter", t);
setErrorMessage(t.getMessage());
fireChangeEvent(DebugEvent.STATE);
return null;
});
}
@Override
public boolean isSuspended() {
return isSuspended;
}
@Override
public boolean canSuspend() {
return !isSuspended();
}
@Override
public String getModelIdentifier() {
return getDebugTarget().getModelIdentifier();
}
@Override
public ILaunch getLaunch() {
return getDebugTarget().getLaunch();
}
@Override
public boolean hasStackFrames() throws DebugException {
return isSuspended();
}
@Override
public IStackFrame getTopStackFrame() throws DebugException {
IStackFrame[] stackFrames = getStackFrames();
if (stackFrames.length > 0) {
return stackFrames[0];
} else {
return null;
}
}
@Override
public IStackFrame[] getStackFrames() throws DebugException {
if (!refreshFrames.getAndSet(false)) {
synchronized (frames) {
return frames.toArray(new DSPStackFrame[frames.size()]);
}
}
try {
StackTraceArguments arguments = new StackTraceArguments();
arguments.setThreadId(id);
// TODO implement paging to get rest of frames
arguments.setStartFrame(0);
arguments.setLevels(20);
CompletableFuture<DSPStackFrame[]> future = getDebugTarget().getDebugProtocolServer().stackTrace(arguments)
.thenApply(response -> {
synchronized (frames) {
StackFrame[] backendFrames = response.getStackFrames();
for (int i = 0; i < backendFrames.length; i++) {
if (i < frames.size()) {
frames.set(i, frames.get(i).replace(backendFrames[i], i));
} else {
frames.add(new DSPStackFrame(this, backendFrames[i], i));
}
}
frames.subList(backendFrames.length, frames.size()).clear();
return frames.toArray(new DSPStackFrame[frames.size()]);
}
});
return future.get();
} catch (RuntimeException | ExecutionException e) {
if (isTerminated()) {
return new DSPStackFrame[0];
}
throw newTargetRequestFailedException(e.getMessage(), e);
} catch (InterruptedException e) {
java.lang.Thread.currentThread().interrupt();
return new DSPStackFrame[0];
}
}
@Override
public int getPriority() throws DebugException {
return 0;
}
@Override
public String getName() {
if (name == null) {
// Queue up a refresh of the threads to get our name
getDebugTarget().getThreads();
return "<pending>";
}
return name;
}
@Override
public IBreakpoint[] getBreakpoints() {
// TODO update breakpoints from stopped messages from server
return new IBreakpoint[0];
}
public Integer getId() {
return id;
}
}