/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.plugin.debugger.ide.debug;
import com.google.common.base.Optional;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.web.bindery.event.shared.EventBus;
import org.eclipse.che.api.core.jsonrpc.commons.RequestTransmitter;
import org.eclipse.che.api.core.jsonrpc.commons.RequestHandlerConfigurator;
import org.eclipse.che.api.debug.shared.dto.BreakpointDto;
import org.eclipse.che.api.debug.shared.dto.DebugSessionDto;
import org.eclipse.che.api.debug.shared.dto.LocationDto;
import org.eclipse.che.api.debug.shared.dto.SimpleValueDto;
import org.eclipse.che.api.debug.shared.dto.StackFrameDumpDto;
import org.eclipse.che.api.debug.shared.dto.VariableDto;
import org.eclipse.che.api.debug.shared.dto.VariablePathDto;
import org.eclipse.che.api.debug.shared.dto.action.ResumeActionDto;
import org.eclipse.che.api.debug.shared.dto.action.StartActionDto;
import org.eclipse.che.api.debug.shared.dto.action.StepIntoActionDto;
import org.eclipse.che.api.debug.shared.dto.action.StepOutActionDto;
import org.eclipse.che.api.debug.shared.dto.action.StepOverActionDto;
import org.eclipse.che.api.debug.shared.dto.action.SuspendActionDto;
import org.eclipse.che.api.debug.shared.dto.event.BreakpointActivatedEventDto;
import org.eclipse.che.api.debug.shared.dto.event.DebuggerEventDto;
import org.eclipse.che.api.debug.shared.dto.event.DisconnectEventDto;
import org.eclipse.che.api.debug.shared.dto.event.SuspendEventDto;
import org.eclipse.che.api.debug.shared.model.DebuggerInfo;
import org.eclipse.che.api.debug.shared.model.Location;
import org.eclipse.che.api.debug.shared.model.SimpleValue;
import org.eclipse.che.api.debug.shared.model.StackFrameDump;
import org.eclipse.che.api.debug.shared.model.Variable;
import org.eclipse.che.api.debug.shared.model.VariablePath;
import org.eclipse.che.api.debug.shared.model.action.Action;
import org.eclipse.che.api.debug.shared.model.impl.SimpleValueImpl;
import org.eclipse.che.api.debug.shared.model.impl.StackFrameDumpImpl;
import org.eclipse.che.api.promises.client.Function;
import org.eclipse.che.api.promises.client.Operation;
import org.eclipse.che.api.promises.client.OperationException;
import org.eclipse.che.api.promises.client.Promise;
import org.eclipse.che.api.promises.client.PromiseError;
import org.eclipse.che.api.promises.client.js.JsPromiseError;
import org.eclipse.che.api.promises.client.js.Promises;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.ide.api.debug.Breakpoint;
import org.eclipse.che.ide.api.debug.BreakpointManager;
import org.eclipse.che.ide.api.debug.DebuggerServiceClient;
import org.eclipse.che.ide.api.machine.events.WsAgentStateEvent;
import org.eclipse.che.ide.api.machine.events.WsAgentStateHandler;
import org.eclipse.che.ide.api.notification.NotificationManager;
import org.eclipse.che.ide.api.resources.Project;
import org.eclipse.che.ide.api.resources.Resource;
import org.eclipse.che.ide.api.resources.VirtualFile;
import org.eclipse.che.ide.debug.Debugger;
import org.eclipse.che.ide.debug.DebuggerDescriptor;
import org.eclipse.che.ide.debug.DebuggerManager;
import org.eclipse.che.ide.debug.DebuggerObservable;
import org.eclipse.che.ide.debug.DebuggerObserver;
import org.eclipse.che.ide.dto.DtoFactory;
import org.eclipse.che.ide.util.loging.Log;
import org.eclipse.che.ide.util.storage.LocalStorage;
import org.eclipse.che.ide.util.storage.LocalStorageProvider;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE;
import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL;
/**
* The common debugger.
*
* @author Anatoliy Bazko
*/
public abstract class AbstractDebugger implements Debugger, DebuggerObservable {
public static final String LOCAL_STORAGE_DEBUGGER_SESSION_KEY = "che-debugger-session";
public static final String LOCAL_STORAGE_DEBUGGER_STATE_KEY = "che-debugger-state";
public static final String WS_AGENT_ENDPOINT = "ws-agent";
public static final String EVENT_DEBUGGER_MESSAGE_BREAKPOINT = "event:debugger:breakpoint";
public static final String EVENT_DEBUGGER_MESSAGE_DISCONNECT = "event:debugger:disconnect";
public static final String EVENT_DEBUGGER_MESSAGE_SUSPEND = "event:debugger:suspend";
public static final String EVENT_DEBUGGER_UN_SUBSCRIBE = "event:debugger:un-subscribe";
public static final String EVENT_DEBUGGER_SUBSCRIBE = "event:debugger:subscribe";
protected final DtoFactory dtoFactory;
protected final NotificationManager notificationManager;
private final List<DebuggerObserver> observers;
private final RequestTransmitter transmitter;
private final RequestHandlerConfigurator configurator;
private final DebuggerServiceClient service;
private final LocalStorageProvider localStorageProvider;
private final EventBus eventBus;
private final ActiveFileHandler activeFileHandler;
private final DebuggerManager debuggerManager;
private final BreakpointManager breakpointManager;
private final String debuggerType;
private DebugSessionDto debugSessionDto;
private Location currentLocation;
public AbstractDebugger(DebuggerServiceClient service,
RequestTransmitter transmitter,
RequestHandlerConfigurator configurator,
DtoFactory dtoFactory,
LocalStorageProvider localStorageProvider,
EventBus eventBus,
ActiveFileHandler activeFileHandler,
DebuggerManager debuggerManager,
NotificationManager notificationManager,
BreakpointManager breakpointManager,
String type) {
this.service = service;
this.transmitter = transmitter;
this.configurator = configurator;
this.dtoFactory = dtoFactory;
this.localStorageProvider = localStorageProvider;
this.eventBus = eventBus;
this.activeFileHandler = activeFileHandler;
this.debuggerManager = debuggerManager;
this.notificationManager = notificationManager;
this.breakpointManager = breakpointManager;
this.observers = new ArrayList<>();
this.debuggerType = type;
restoreDebuggerState();
addHandlers();
}
private void addHandlers() {
eventBus.addHandler(WsAgentStateEvent.TYPE, new WsAgentStateHandler() {
@Override
public void onWsAgentStarted(WsAgentStateEvent event) {
transmitter.newRequest()
.endpointId(WS_AGENT_ENDPOINT)
.methodName(EVENT_DEBUGGER_SUBSCRIBE)
.noParams()
.sendAndSkipResult();
if (!isConnected()) {
return;
}
Promise<DebugSessionDto> promise = service.getSessionInfo(debugSessionDto.getId());
promise.then(debugSessionDto -> {
debuggerManager.setActiveDebugger(AbstractDebugger.this);
setDebugSession(debugSessionDto);
DebuggerInfo debuggerInfo = debugSessionDto.getDebuggerInfo();
String info = debuggerInfo.getName() + " " + debuggerInfo.getVersion();
String address = debuggerInfo.getHost() + ":" + debuggerInfo.getPort();
DebuggerDescriptor debuggerDescriptor = new DebuggerDescriptor(info, address);
for (DebuggerObserver observer : observers) {
observer.onDebuggerAttached(debuggerDescriptor, Promises.resolve(null));
}
for (BreakpointDto breakpoint : debugSessionDto.getBreakpoints()) {
onBreakpointActivated(breakpoint.getLocation());
}
if (currentLocation != null) {
openCurrentFile();
}
startCheckingEvents();
}).catchError(error -> {
disconnect();
});
}
@Override
public void onWsAgentStopped(WsAgentStateEvent event) {
}
});
}
private void onEventListReceived(@NotNull DebuggerEventDto event) {
LocationDto newLocationDto;
switch (event.getType()) {
case SUSPEND:
newLocationDto = ((SuspendEventDto)event).getLocation();
break;
case BREAKPOINT_ACTIVATED:
BreakpointDto breakpointDto = ((BreakpointActivatedEventDto)event).getBreakpoint();
onBreakpointActivated(breakpointDto.getLocation());
return;
case DISCONNECT:
disconnect();
return;
default:
Log.error(AbstractDebugger.class, "Unknown debuggerType of debugger event: " + event.getType());
return;
}
if (newLocationDto != null) {
currentLocation = newLocationDto;
openCurrentFile();
}
preserveDebuggerState();
}
private void openCurrentFile() {
//todo we need add possibility to handle few files
activeFileHandler.openFile(currentLocation,
new AsyncCallback<VirtualFile>() {
@Override
public void onFailure(Throwable caught) {
for (DebuggerObserver observer : observers) {
observer.onBreakpointStopped(currentLocation.getTarget(),
currentLocation.getTarget(),
currentLocation.getLineNumber());
}
}
@Override
public void onSuccess(VirtualFile result) {
for (DebuggerObserver observer : observers) {
observer.onBreakpointStopped(result.getLocation().toString(),
currentLocation.getTarget(),
currentLocation.getLineNumber());
}
}
});
}
/**
* Breakpoint became active. It might happens because of different reasons:
* <li>breakpoint was deferred and VM eventually loaded class and added it</li>
* <li>condition triggered</li>
* <li>etc</li>
*/
private void onBreakpointActivated(LocationDto locationDto) {
String filePath = fqnToPath(locationDto);
for (DebuggerObserver observer : observers) {
observer.onBreakpointActivated(filePath, locationDto.getLineNumber() - 1);
}
}
private void startCheckingEvents() {
configurator.newConfiguration()
.methodName(EVENT_DEBUGGER_MESSAGE_SUSPEND)
.paramsAsDto(SuspendEventDto.class)
.noResult()
.withConsumer((endpointId, event) -> {
Log.debug(getClass(), "Received suspend message from endpoint: " + endpointId);
onEventListReceived(event);
});
configurator.newConfiguration()
.methodName(EVENT_DEBUGGER_MESSAGE_DISCONNECT)
.paramsAsDto(DisconnectEventDto.class)
.noResult()
.withConsumer((endpointId, event) -> {
Log.debug(getClass(), "Received disconnect message from endpoint: " + endpointId);
onEventListReceived(event);
});
configurator.newConfiguration()
.methodName(EVENT_DEBUGGER_MESSAGE_BREAKPOINT)
.paramsAsDto(BreakpointActivatedEventDto.class)
.noResult()
.withConsumer((endpointId, event) -> {
Log.debug(getClass(), "Received breakpoint activated message from endpoint: " + endpointId);
onEventListReceived(event);
});
}
private void stopCheckingDebugEvents() {
transmitter.newRequest()
.endpointId(WS_AGENT_ENDPOINT)
.methodName(EVENT_DEBUGGER_UN_SUBSCRIBE)
.noParams()
.sendAndSkipResult();
}
@Override
public Promise<SimpleValue> getValue(Variable variable) {
if (!isConnected()) {
return Promises.reject(JsPromiseError.create("Debugger is not connected"));
}
Promise<SimpleValueDto> promise = service.getValue(debugSessionDto.getId(), asDto(variable));
return promise.then((Function<SimpleValueDto, SimpleValue>)SimpleValueImpl::new);
}
@Override
public Promise<StackFrameDump> dumpStackFrame() {
if (!isConnected()) {
return Promises.reject(JsPromiseError.create("Debugger is not connected"));
}
Promise<StackFrameDumpDto> stackFrameDump = service.getStackFrameDump(debugSessionDto.getId());
return stackFrameDump.then((Function<StackFrameDumpDto, StackFrameDump>)StackFrameDumpImpl::new);
}
@Override
public void addBreakpoint(final VirtualFile file, final int lineNumber) {
if (isConnected()) {
String fqn = pathToFqn(file);
if (fqn == null) {
return;
}
final String filePath = file.getLocation().toString();
LocationDto locationDto = dtoFactory.createDto(LocationDto.class)
.withLineNumber(lineNumber + 1)
.withTarget(fqn)
.withResourcePath(filePath)
.withResourceProjectPath(getProject(file).getPath());
BreakpointDto breakpointDto = dtoFactory.createDto(BreakpointDto.class).withLocation(locationDto).withEnabled(true);
Promise<Void> promise = service.addBreakpoint(debugSessionDto.getId(), breakpointDto);
promise.then(it -> {
Breakpoint breakpoint = new Breakpoint(Breakpoint.Type.BREAKPOINT, lineNumber, filePath, file, true);
for (DebuggerObserver observer : observers) {
observer.onBreakpointAdded(breakpoint);
}
}).catchError(error -> {
Log.error(AbstractDebugger.class, error.getMessage());
});
} else {
Breakpoint breakpoint = new Breakpoint(Breakpoint.Type.BREAKPOINT, lineNumber, file.getLocation().toString(), file, false);
for (DebuggerObserver observer : observers) {
observer.onBreakpointAdded(breakpoint);
}
}
}
@Override
public void deleteBreakpoint(final VirtualFile file, final int lineNumber) {
if (!isConnected()) {
return;
}
LocationDto locationDto = dtoFactory.createDto(LocationDto.class);
locationDto.setLineNumber(lineNumber + 1);
String fqn = pathToFqn(file);
if (fqn == null) {
return;
}
locationDto.setTarget(fqn);
Promise<Void> promise = service.deleteBreakpoint(debugSessionDto.getId(), locationDto);
promise.then(it -> {
for (DebuggerObserver observer : observers) {
Breakpoint breakpoint =
new Breakpoint(Breakpoint.Type.BREAKPOINT, lineNumber, file.getLocation().toString(), file, false);
observer.onBreakpointDeleted(breakpoint);
}
}).catchError(error -> {
Log.error(AbstractDebugger.class, error.getMessage());
});
}
@Override
public void deleteAllBreakpoints() {
if (!isConnected()) {
return;
}
Promise<Void> promise = service.deleteAllBreakpoints(debugSessionDto.getId());
promise.then(it -> {
for (DebuggerObserver observer : observers) {
observer.onAllBreakpointsDeleted();
}
}).catchError(error -> {
Log.error(AbstractDebugger.class, error.getMessage());
});
}
@Override
public Promise<Void> connect(Map<String, String> connectionProperties) {
if (isConnected()) {
return Promises.reject(JsPromiseError.create("Debugger already connected"));
}
Promise<DebugSessionDto> connect = service.connect(debuggerType, connectionProperties);
final DebuggerDescriptor debuggerDescriptor = toDescriptor(connectionProperties);
Promise<Void> promise = connect.then((Function<DebugSessionDto, Void>)debugSession -> {
DebuggerInfo debuggerInfo = debugSession.getDebuggerInfo();
debuggerDescriptor.setInfo(debuggerInfo.getName() + " " + debuggerInfo.getVersion());
setDebugSession(debugSession);
preserveDebuggerState();
startCheckingEvents();
startDebugger(debugSession);
return null;
}).catchError((Operation<PromiseError>)error -> {
Log.error(AbstractDebugger.class, error.getMessage());
throw new OperationException(error.getCause());
});
for (DebuggerObserver observer : observers) {
observer.onDebuggerAttached(debuggerDescriptor, promise);
}
return promise;
}
protected void startDebugger(final DebugSessionDto debugSessionDto) {
List<BreakpointDto> breakpoints = new ArrayList<>();
for (Breakpoint breakpoint : breakpointManager.getBreakpointList()) {
LocationDto locationDto = dtoFactory.createDto(LocationDto.class)
.withLineNumber(breakpoint.getLineNumber() + 1)
.withResourcePath(breakpoint.getPath())
.withResourceProjectPath(getProject(breakpoint.getFile()).getPath());
String target = pathToFqn(breakpoint.getFile());
if (target != null) {
locationDto.setTarget(target);
BreakpointDto breakpointDto = dtoFactory.createDto(BreakpointDto.class);
breakpointDto.setLocation(locationDto);
breakpointDto.setEnabled(true);
breakpoints.add(breakpointDto);
}
}
StartActionDto action = dtoFactory.createDto(StartActionDto.class);
action.setType(Action.TYPE.START);
action.setBreakpoints(breakpoints);
service.start(debugSessionDto.getId(), action);
}
@Override
public void disconnect() {
stopCheckingDebugEvents();
Promise<Void> disconnect;
if (isConnected()) {
disconnect = service.disconnect(debugSessionDto.getId());
} else {
disconnect = Promises.resolve(null);
}
invalidateDebugSession();
preserveDebuggerState();
disconnect.then(it -> {
for (DebuggerObserver observer : observers) {
observer.onDebuggerDisconnected();
}
debuggerManager.setActiveDebugger(null);
}).catchError(error -> {
for (DebuggerObserver observer : observers) {
observer.onDebuggerDisconnected();
}
debuggerManager.setActiveDebugger(null);
});
}
@Override
public void stepInto() {
if (isConnected()) {
for (DebuggerObserver observer : observers) {
observer.onPreStepInto();
}
removeCurrentLocation();
preserveDebuggerState();
StepIntoActionDto action = dtoFactory.createDto(StepIntoActionDto.class);
action.setType(Action.TYPE.STEP_INTO);
Promise<Void> promise = service.stepInto(debugSessionDto.getId(), action);
promise.catchError(error -> {
Log.error(AbstractDebugger.class, error.getCause());
});
}
}
@Override
public void stepOver() {
if (isConnected()) {
for (DebuggerObserver observer : observers) {
observer.onPreStepOver();
}
removeCurrentLocation();
preserveDebuggerState();
StepOverActionDto action = dtoFactory.createDto(StepOverActionDto.class);
action.setType(Action.TYPE.STEP_OVER);
Promise<Void> promise = service.stepOver(debugSessionDto.getId(), action);
promise.catchError(error -> {
Log.error(AbstractDebugger.class, error.getCause());
});
}
}
@Override
public void stepOut() {
if (isConnected()) {
for (DebuggerObserver observer : observers) {
observer.onPreStepOut();
}
removeCurrentLocation();
preserveDebuggerState();
StepOutActionDto action = dtoFactory.createDto(StepOutActionDto.class);
action.setType(Action.TYPE.STEP_OUT);
Promise<Void> promise = service.stepOut(debugSessionDto.getId(), action);
promise.catchError(error -> {
Log.error(AbstractDebugger.class, error.getCause());
});
}
}
@Override
public void resume() {
if (isConnected()) {
for (DebuggerObserver observer : observers) {
observer.onPreResume();
}
removeCurrentLocation();
preserveDebuggerState();
ResumeActionDto action = dtoFactory.createDto(ResumeActionDto.class);
action.setType(Action.TYPE.RESUME);
Promise<Void> promise = service.resume(debugSessionDto.getId(), action);
promise.catchError(error -> {
Log.error(AbstractDebugger.class, error.getCause());
});
}
}
@Override
public void suspend() {
if (!isConnected()) {
return;
}
SuspendActionDto suspendAction = dtoFactory.createDto(SuspendActionDto.class);
suspendAction.setType(Action.TYPE.SUSPEND);
service.suspend(debugSessionDto.getId(), suspendAction).catchError(error -> {
notificationManager.notify(error.getMessage(), FAIL, FLOAT_MODE);
});
}
@Override
public Promise<String> evaluate(String expression) {
if (isConnected()) {
return service.evaluate(debugSessionDto.getId(), expression);
}
return Promises.reject(JsPromiseError.create("Debugger is not connected"));
}
@Override
public void setValue(final Variable variable) {
if (isConnected()) {
Promise<Void> promise = service.setValue(debugSessionDto.getId(), asDto(variable));
promise.then(it -> {
for (DebuggerObserver observer : observers) {
observer.onValueChanged(variable.getVariablePath().getPath(), variable.getValue());
}
}).catchError(error -> {
Log.error(AbstractDebugger.class, error.getMessage());
});
}
}
@Override
public boolean isConnected() {
return debugSessionDto != null;
}
@Override
public boolean isSuspended() {
return isConnected() && currentLocation != null;
}
@Override
public String getDebuggerType() {
return debuggerType;
}
@Override
public void addObserver(DebuggerObserver observer) {
observers.add(observer);
}
@Override
public void removeObserver(DebuggerObserver observer) {
observers.remove(observer);
}
protected void setDebugSession(DebugSessionDto debugSessionDto) {
this.debugSessionDto = debugSessionDto;
}
private void invalidateDebugSession() {
this.debugSessionDto = null;
this.removeCurrentLocation();
}
private void removeCurrentLocation() {
currentLocation = null;
}
/**
* Preserves debugger information into the local storage.
*/
protected void preserveDebuggerState() {
LocalStorage localStorage = localStorageProvider.get();
if (localStorage == null) {
return;
}
if (!isConnected()) {
localStorage.setItem(LOCAL_STORAGE_DEBUGGER_SESSION_KEY, "");
localStorage.setItem(LOCAL_STORAGE_DEBUGGER_STATE_KEY, "");
} else {
localStorage.setItem(LOCAL_STORAGE_DEBUGGER_SESSION_KEY, dtoFactory.toJson(debugSessionDto));
if (currentLocation == null) {
localStorage.setItem(LOCAL_STORAGE_DEBUGGER_STATE_KEY, "");
} else {
localStorage.setItem(LOCAL_STORAGE_DEBUGGER_STATE_KEY, dtoFactory.toJson(currentLocation));
}
}
}
/**
* Loads debugger information from the local storage.
*/
protected void restoreDebuggerState() {
invalidateDebugSession();
LocalStorage localStorage = localStorageProvider.get();
if (localStorage == null) {
return;
}
String data = localStorage.getItem(LOCAL_STORAGE_DEBUGGER_SESSION_KEY);
if (data != null && !data.isEmpty()) {
DebugSessionDto debugSessionDto = dtoFactory.createDtoFromJson(data, DebugSessionDto.class);
if (!debugSessionDto.getType().equals(getDebuggerType())) {
return;
}
setDebugSession(debugSessionDto);
}
data = localStorage.getItem(LOCAL_STORAGE_DEBUGGER_STATE_KEY);
if (data != null && !data.isEmpty()) {
currentLocation = dtoFactory.createDtoFromJson(data, LocationDto.class);
}
}
private VariableDto asDto(Variable variable) {
VariableDto dto = dtoFactory.createDto(VariableDto.class);
dto.withValue(variable.getValue());
dto.withVariablePath(asDto(variable.getVariablePath()));
dto.withPrimitive(variable.isPrimitive());
dto.withType(variable.getType());
dto.withName(variable.getName());
dto.withExistInformation(variable.isExistInformation());
dto.withVariables(asDto(variable.getVariables()));
return dto;
}
private List<VariableDto> asDto(List<? extends Variable> variables) {
List<VariableDto> dtos = new ArrayList<>(variables.size());
for (Variable v : variables) {
dtos.add(asDto(v));
}
return dtos;
}
private VariablePathDto asDto(VariablePath variablePath) {
return dtoFactory.createDto(VariablePathDto.class).withPath(variablePath.getPath());
}
@Nullable
private Project getProject(VirtualFile virtualFile) {
if (virtualFile instanceof Resource) {
Optional<Project> projectOptional = ((Resource)virtualFile).getRelatedProject();
if (projectOptional.isPresent()) {
return projectOptional.get();
}
}
return null;
}
/**
* Transforms FQN to file path.
*/
abstract protected String fqnToPath(@NotNull Location location);
/**
* Transforms file path to FQN>
*/
@Nullable
abstract protected String pathToFqn(VirtualFile file);
abstract protected DebuggerDescriptor toDescriptor(Map<String, String> connectionProperties);
}