blob: 87d6587cd7046a925341a58573cd3f013eba7d88 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.cpplite.debugger;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.debugger.ActionsManager;
import org.netbeans.api.debugger.Breakpoint;
import org.netbeans.api.debugger.DebuggerEngine;
import org.netbeans.api.debugger.DebuggerInfo;
import org.netbeans.api.debugger.DebuggerManager;
import org.netbeans.api.debugger.DebuggerManagerAdapter;
import org.netbeans.modules.cnd.debugger.gdb2.mi.MICommand;
import org.netbeans.modules.cnd.debugger.gdb2.mi.MICommandInjector;
import org.netbeans.modules.cnd.debugger.gdb2.mi.MIConst;
import org.netbeans.modules.cnd.debugger.gdb2.mi.MIProxy;
import org.netbeans.modules.cnd.debugger.gdb2.mi.MIRecord;
import org.netbeans.modules.cnd.debugger.gdb2.mi.MITList;
import org.netbeans.modules.cnd.debugger.gdb2.mi.MIValue;
import org.netbeans.modules.cpplite.debugger.breakpoints.CPPLiteBreakpoint;
import org.netbeans.modules.nativeexecution.api.ExecutionEnvironmentFactory;
import org.netbeans.modules.nativeexecution.api.pty.Pty;
import org.netbeans.modules.nativeexecution.api.pty.PtySupport;
import org.netbeans.spi.debugger.ContextProvider;
import org.netbeans.spi.debugger.DebuggerEngineProvider;
import org.netbeans.spi.debugger.SessionProvider;
import org.netbeans.spi.debugger.ui.DebuggingView;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.text.Annotatable;
import org.openide.text.Line;
import org.openide.util.Exceptions;
import org.openide.util.Pair;
import org.openide.util.RequestProcessor;
/**
* C/C++ lite debugger.
*
* @author Honza
*/
public final class CPPLiteDebugger {
private static final Logger LOGGER = Logger.getLogger(CPPLiteDebugger.class.getName());
private CPPLiteDebuggerConfig configuration;
private CPPLiteDebuggerEngineProvider engineProvider;
private ContextProvider contextProvider;
private Process debuggee;
private LiteMIProxy proxy;
private volatile Object currentLine;
private volatile boolean suspended = false;
private final List<StateListener> stateListeners = new CopyOnWriteArrayList<>();
private final BreakpointsHandler breakpointsHandler = new BreakpointsHandler();
private final ThreadsCollector threadsCollector = new ThreadsCollector(this);
private volatile CPPThread currentThread;
private volatile CPPFrame currentFrame;
public CPPLiteDebugger(ContextProvider contextProvider) {
this.contextProvider = contextProvider;
configuration = contextProvider.lookupFirst(null, CPPLiteDebuggerConfig.class);
// init engineProvider
engineProvider = (CPPLiteDebuggerEngineProvider) contextProvider.lookupFirst(null, DebuggerEngineProvider.class);
}
void setDebuggee(Process debuggee) {
this.debuggee = debuggee;
CPPLiteInjector injector = new CPPLiteInjector(debuggee.getOutputStream());
this.proxy = new LiteMIProxy(injector, "(gdb)", "UTF-8");
new Thread(() -> {
try (BufferedReader r = new BufferedReader(new InputStreamReader(debuggee.getInputStream()))) {
String line;
while ((line = r.readLine()) != null) {
proxy.processLine(line);
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}).start();
proxy.waitStarted();
breakpointsHandler.init();
proxy.send(new Command("-gdb-set target-async"));
//proxy.send(new Command("-gdb-set scheduler-locking on"));
proxy.send(new Command("-gdb-set non-stop on"));
proxy.send(new Command("-exec-run"));
}
private static class CPPLiteInjector implements MICommandInjector {
private final OutputStream out;
public CPPLiteInjector(OutputStream out) {
this.out = out;
}
@Override
public synchronized void inject(String data) { // inject must not be called concurrently
LOGGER.log(Level.FINE, "CPPLiteInjector.inject({0})", data);
try {
out.write(data.getBytes());
out.flush();
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
@Override
public void log(String data) {
LOGGER.log(Level.FINE, "CPPLiteInjector.log({0})", data);
}
}
MIRecord sendAndGet(String command) throws InterruptedException {
return sendAndGet(command, false);
}
MIRecord sendAndGet(String command, boolean waitForRunning) throws InterruptedException {
CountDownLatch done = new CountDownLatch(1);
MIRecord[] result = new MIRecord[1];
proxy.send(new Command(command) {
@Override
protected void onDone(MIRecord record) {
result[0] = record;
done.countDown();
}
@Override
protected void onError(MIRecord record) {
result[0] = record;
done.countDown();
}
@Override
protected void onExit(MIRecord record) {
result[0] = record;
done.countDown();
}
}, waitForRunning);
done.await();
return result[0];
}
void send(Command command) {
proxy.send(command);
}
// other methods ...........................................................
public boolean isSuspended() {
return suspended;
}
private void setSuspended(boolean suspended, CPPThread thread, CPPFrame frame) {
boolean suspendedOld;
boolean suspendedNew;
CPPThread currentThreadOld;
CPPThread currentThreadNew;
CPPFrame currentFrameOld;
CPPFrame currentFrameNew;
synchronized (this) {
suspendedNew = suspendedOld = this.suspended;
currentThreadNew = currentThreadOld = this.currentThread;
currentFrameNew = currentFrameOld = this.currentFrame;
if (suspended) {
if (currentThreadOld == null || currentThreadOld.getStatus() != CPPThread.Status.SUSPENDED) {
currentThreadNew = thread;
currentFrameNew = frame;
} else if (currentThreadOld == thread) {
currentFrameNew = frame;
}
suspendedNew = true;
} else {
if (thread == currentThreadOld) {
suspendedNew = false;
currentFrameNew = null;
}
}
this.suspended = suspendedNew;
this.currentThread = currentThreadNew;
this.currentFrame = currentFrameNew;
}
if (suspendedNew != suspendedOld) {
for (StateListener sl : stateListeners) {
sl.suspended(suspendedNew);
}
}
if (currentThreadNew != currentThreadOld) {
for (StateListener sl : stateListeners) {
sl.currentThread(currentThreadNew);
}
}
if (currentFrameNew != currentFrameOld) {
for (StateListener sl : stateListeners) {
sl.currentFrame(currentFrameNew);
}
}
}
private void fireFinished() {
for (StateListener sl : stateListeners) {
sl.finished();
}
}
public void addStateListener(StateListener sl) {
stateListeners.add(sl);
}
public void removeStateListener(StateListener sl) {
stateListeners.remove(sl);
}
public ThreadsCollector getThreads() {
return threadsCollector;
}
public CPPThread getCurrentThread() {
return currentThread;
}
public CPPFrame getCurrentFrame() {
return currentFrame;
}
void setCurrentStackFrame(CPPFrame cppFrame) {
CPPThread currentThreadOld;
CPPFrame currentFrameOld;
CPPThread currentThreadNew = cppFrame.getThread();
synchronized (this) {
currentThreadOld = this.currentThread;
currentFrameOld = this.currentFrame;
this.currentThread = currentThreadNew;
this.currentFrame = cppFrame;
}
if (currentThreadNew != currentThreadOld) {
for (StateListener sl : stateListeners) {
sl.currentThread(currentThreadNew);
}
}
if (cppFrame != currentFrameOld) {
for (StateListener sl : stateListeners) {
sl.currentFrame(cppFrame);
}
}
}
void setCurrentThread(CPPThread thread) {
CPPThread currentThreadOld;
CPPFrame currentFrameOld;
CPPFrame currentFrameNew;
synchronized (this) {
currentThreadOld = this.currentThread;
if (currentThreadOld == thread) {
return;
}
this.currentThread = thread;
currentFrameOld = this.currentFrame;
this.currentFrame = currentFrameNew = thread.getTopFrame();
}
if (thread != currentThreadOld) {
for (StateListener sl : stateListeners) {
sl.currentThread(thread);
}
}
if (currentFrameNew != currentFrameOld) {
for (StateListener sl : stateListeners) {
sl.currentFrame(currentFrameNew);
}
}
}
public Object getCurrentLine () {
return currentLine;
}
private volatile boolean finished = false; // When the debugger has finished.
public boolean isFinished() {
return finished;
}
/**
* should define callStack based on callStackInternal & action.
*/
void doStep (Object action) {
CPPThread thread = currentThread;
String threadId = "";
if (thread != null) {
thread.notifyRunning();
threadId = " --thread " + thread.getId();
}
if (action == ActionsManager.ACTION_STEP_OVER) {
proxy.send(new Command("-exec-next" + threadId));
} else if (action == ActionsManager.ACTION_STEP_INTO) {
proxy.send(new Command("-exec-step" + threadId));
} else if (action == ActionsManager.ACTION_STEP_OUT) {
proxy.send(new Command("-exec-finish" + threadId));
}
}
void pause() {
proxy.send(new Command("-exec-interrupt --all"));
}
void resume() {
threadsCollector.running("all");
proxy.send(new Command("-exec-continue --all"));
}
void finish () {
LOGGER.fine("CPPLiteDebugger.finish()");
if (finished) {
LOGGER.fine("finish(): already finished.");
return ;
}
breakpointsHandler.dispose();
proxy.send(new Command("-gdb-exit"));
Utils.unmarkCurrent ();
engineProvider.getDestructor().killEngine();
finished = true;
fireFinished();
LOGGER.fine("finish() done, build finished.");
}
DebuggingView.DVSupport getDVSupport() {
return contextProvider.lookupFirst(null, DebuggingView.DVSupport.class);
}
private class LiteMIProxy extends MIProxy {
private final CountDownLatch startedLatch = new CountDownLatch(1);
private final CountDownLatch runningLatch = new CountDownLatch(1);
private final CountDownLatch runningCommandLatch = new CountDownLatch(0);
private final Semaphore runningCommandSemaphore = new Semaphore(1);
LiteMIProxy(MICommandInjector injector, String prompt, String encoding) {
super(injector, prompt, encoding);
}
@Override
protected void prompt() {
startedLatch.countDown();
}
@Override
protected void execAsyncOutput(MIRecord record) {
LOGGER.log(Level.FINE, "MIProxy.execAsyncOutput({0})", record);
//if (record.token() == 0) {
switch (record.cls()) {
case "stopped":
MITList results = record.results();
String threadId = results.getConstValue("thread-id");
MIValue stoppedThreads = results.valueOf("stopped-threads");
if (stoppedThreads != null) {
if (stoppedThreads.isConst()) {
threadsCollector.stopped(stoppedThreads.asConst().value());
} else {
MITList stoppedThreadsList = stoppedThreads.asList();
int size = stoppedThreadsList.size();
String[] ids = new String[size];
for (int i = 0; i < size; i++) {
ids[i] = ((MIConst) stoppedThreadsList.get(i)).value();
}
threadsCollector.stopped(ids);
}
}
CPPThread thread = threadsCollector.get(threadId);
String reason = results.getConstValue("reason", "");
switch (reason) {
case "exited-normally":
if ('*' == record.type()) {
finish();
} else {
threadsCollector.remove(threadId);
}
break;
default:
MITList topFrameList = (MITList) results.valueOf("frame");
CPPFrame frame = topFrameList != null ? new CPPFrame(thread, topFrameList) : null;
thread.setTopFrame(frame);
setSuspended(true, thread, frame);
if (frame != null) {
Line currentLine = frame.location();
if (currentLine != null) {
Annotatable[] lines = new Annotatable[] {currentLine};
CPPLiteDebugger.this.currentLine = lines;
Utils.markCurrent(lines);
Utils.showLine(lines);
}
}
break;
}
break;
case "running":
results = record.results();
threadId = results.getConstValue("thread-id");
thread = threadsCollector.running(threadId);
setSuspended(false, thread, null);
Utils.unmarkCurrent();
break;
default:
//unknown class, ignore
break;
}
return;
//}
//super.execAsyncOutput(record);
}
@Override
protected void notifyAsyncOutput(MIRecord record) {
LOGGER.log(Level.FINE, "MIProxy.notifyAsyncOutput({0})", record);
if ('=' == record.type()) {
switch (record.cls()) {
case "thread-created":
String id = getThreadId(record);
threadsCollector.add(id);
break;
case "thread-exited":
id = getThreadId(record);
threadsCollector.remove(id);
break;
}
}
super.notifyAsyncOutput(record);
}
private String getThreadId(MIRecord record) {
MITList results = record.results();
String id = results.getConstValue("id");
return id;
}
@Override
protected void statusAsyncOutput(MIRecord record) {
LOGGER.log(Level.FINE, "MIProxy.statusAsyncOutput({0})", record);
super.statusAsyncOutput(record);
}
@Override
protected void result(MIRecord record) {
LOGGER.log(Level.FINE, "MIProxy.result({0})", record);
switch (record.cls()) {
case "running":
runningLatch.countDown();
break;
}
runningCommandSemaphore.release();
super.result(record);
}
void send(MICommand cmd, boolean waitForRunning) {
if (waitForRunning) {
waitRunning();
}
send(cmd);
}
@Override
public void send(MICommand cmd) {
try {
startedLatch.await();
runningCommandSemaphore.acquire();
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
LOGGER.log(Level.FINE, "MIProxy.send({0})", cmd);
super.send(cmd);
}
@Override
public boolean processLine(String line) {
LOGGER.log(Level.FINER, "MIProxy.processLine({0})", line);
return super.processLine(line);
}
void waitStarted() {
try {
startedLatch.await();
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
}
void waitRunning() {
try {
runningLatch.await();
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
}
}
public interface StateListener extends EventListener {
void currentThread(CPPThread thread);
void currentFrame(CPPFrame frame);
void suspended(boolean suspended);
void finished();
}
public static @NonNull Pair<DebuggerEngine, Process> startDebugging (CPPLiteDebuggerConfig configuration) throws IOException {
DebuggerInfo di = DebuggerInfo.create (
"CPPLiteDebuggerInfo",
new Object[] {
new SessionProvider () {
@Override
public String getSessionName () {
return configuration.getDisplayName ();
}
@Override
public String getLocationName () {
return "localhost";
}
@Override
public String getTypeID () {
return "CPPLiteSession";
}
@Override
public Object[] getServices () {
return new Object[] {};
}
},
configuration
}
);
DebuggerEngine[] es = DebuggerManager.getDebuggerManager ().
startDebugging (di);
Pty pty = PtySupport.allocate(ExecutionEnvironmentFactory.getLocal());
CPPLiteDebugger debugger = es[0].lookupFirst(null, CPPLiteDebugger.class);
List<String> executable = new ArrayList<>();
executable.add("gdb");
executable.add("--interpreter=mi");
executable.add("--tty=" + pty.getSlaveName());
executable.addAll(configuration.getExecutable());
Process debuggee = new ProcessBuilder(executable).directory(configuration.getDirectory()).start();
new RequestProcessor(configuration.getDisplayName() + " (pty deallocator)").post(() -> {
try {
while (debuggee.isAlive()) {
try {
debuggee.waitFor();
} catch (InterruptedException ex) {
//ignore...
}
}
} finally {
try {
PtySupport.deallocate(pty);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
});
debugger.setDebuggee(debuggee);
return Pair.of(es[0], new Process() {
@Override
public OutputStream getOutputStream() {
return pty.getOutputStream();
}
@Override
public InputStream getInputStream() {
return pty.getInputStream();
}
@Override
public InputStream getErrorStream() {
return pty.getErrorStream();
}
@Override
public int waitFor() throws InterruptedException {
return debuggee.waitFor();
}
@Override
public int exitValue() {
return debuggee.exitValue();
}
@Override
public void destroy() {
debuggee.destroy();
}
});
}
private class BreakpointsHandler extends DebuggerManagerAdapter implements PropertyChangeListener {
private final Map<String, CPPLiteBreakpoint> breakpointsById = new ConcurrentHashMap<>();
private final Map<CPPLiteBreakpoint, String> breakpointIds = new ConcurrentHashMap<>();
BreakpointsHandler() {
}
private void init() {
DebuggerManager.getDebuggerManager().addDebuggerListener(DebuggerManager.PROP_BREAKPOINTS, this);
for (Breakpoint b : DebuggerManager.getDebuggerManager().getBreakpoints()) {
if (b instanceof CPPLiteBreakpoint) {
CPPLiteBreakpoint cpplineBreakpoint = (CPPLiteBreakpoint) b;
addBreakpoint(cpplineBreakpoint);
}
}
}
void dispose() {
DebuggerManager.getDebuggerManager().removeDebuggerListener(DebuggerManager.PROP_BREAKPOINTS, this);
for (Breakpoint b : DebuggerManager.getDebuggerManager().getBreakpoints()) {
if (b instanceof CPPLiteBreakpoint) {
b.removePropertyChangeListener(this);
}
}
}
@Override
public void breakpointAdded(Breakpoint breakpoint) {
if (breakpoint instanceof CPPLiteBreakpoint) {
addBreakpoint((CPPLiteBreakpoint) breakpoint);
}
}
@Override
public void breakpointRemoved(Breakpoint breakpoint) {
if (breakpoint instanceof CPPLiteBreakpoint) {
removeBreakpoint((CPPLiteBreakpoint) breakpoint);
}
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
Object source = evt.getSource();
if (source instanceof CPPLiteBreakpoint) {
String id = breakpointIds.get((CPPLiteBreakpoint) source);
if (id != null) {
String propertyName = evt.getPropertyName();
switch (propertyName) {
case Breakpoint.PROP_ENABLED:
if (Boolean.TRUE.equals(evt.getNewValue())) {
proxy.send(new Command("-break-enable " + id));
} else {
proxy.send(new Command("-break-disable " + id));
}
break;
}
}
}
}
private void addBreakpoint(CPPLiteBreakpoint breakpoint) {
Line l = breakpoint.getLine();
FileObject source = l.getLookup().lookup(FileObject.class);
File sourceFile = source != null ? FileUtil.toFile(source) : null;
if (sourceFile != null) {
String disabled = breakpoint.isEnabled() ? "" : "-d ";
Command command = new Command("-break-insert " + disabled + sourceFile.getAbsolutePath() + ":" + (l.getLineNumber() + 1)) {
@Override
protected void onDone(MIRecord record) {
MIValue bkpt = record.results().valueOf("bkpt");
if (bkpt instanceof MITList) {
breakpointResolved(breakpoint, (MITList) bkpt);
}
super.onDone(record);
}
@Override
protected void onError(MIRecord record) {
String msg = record.results().getConstValue("msg");
breakpointError(breakpoint, msg);
super.onError(record);
}
};
proxy.send(command);
}
breakpoint.addPropertyChangeListener(this);
}
private void removeBreakpoint(CPPLiteBreakpoint breakpoint) {
String id = breakpointIds.remove(breakpoint);
if (id != null) {
breakpoint.removePropertyChangeListener(this);
Command command = new Command("-break-delete " + id);
proxy.send(command);
}
}
private void breakpointResolved(CPPLiteBreakpoint breakpoint, MITList list) {
breakpoint.setCPPValidity(Breakpoint.VALIDITY.VALID, null);
String id = list.getConstValue("number");
breakpointsById.put(id, breakpoint);
breakpointIds.put(breakpoint, id);
}
private void breakpointError(CPPLiteBreakpoint breakpoint, String msg) {
breakpoint.setCPPValidity(Breakpoint.VALIDITY.INVALID, msg);
}
}
}