blob: 01933785e53e54aacb43dde5289d54fcc4e7f570 [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.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.Iterator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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.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.ActionsProviderSupport;
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.Line;
import org.openide.util.Exceptions;
import org.openide.util.Pair;
import org.openide.util.RequestProcessor;
/**
* C/C++ lite debugger.
*
* @author Honza
*/
public class CPPLiteDebugger extends ActionsProviderSupport {
private static final Logger LOGGER = Logger.getLogger(CPPLiteDebugger.class.getName());
/** The ReqeustProcessor used by action performers. */
private static RequestProcessor actionsRequestProcessor;
private static RequestProcessor killRequestProcessor;
private CPPLiteDebuggerConfig configuration;
private CPPLiteDebuggerEngineProvider engineProvider;
private ContextProvider contextProvider;
private Process debuggee;
private LiteMIProxy proxy;
private Object currentLine;
private volatile boolean suspended = false;
private final List<StateListener> stateListeners = new CopyOnWriteArrayList<>();
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);
// init actions
for (Iterator it = actions.iterator(); it.hasNext(); ) {
setEnabled (it.next(), true);
}
}
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();
for (Breakpoint b : DebuggerManager.getDebuggerManager ().getBreakpoints ()) {
if (b instanceof CPPLiteBreakpoint) {
CPPLiteBreakpoint cpplineBreakpoint = (CPPLiteBreakpoint) b;
Line l = cpplineBreakpoint.getLine();
FileObject source = l.getLookup().lookup(FileObject.class);
File sourceFile = source != null ? FileUtil.toFile(source) : null;
if (sourceFile != null) {
proxy.send(new Command("-break-insert " + sourceFile.getAbsolutePath() + ":" + (l.getLineNumber() + 1)));
}
}
}
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"));
}
// ActionsProvider .........................................................
private static final Set<Object> actions = new HashSet<>();
private static final Set<Object> actionsToDisable = new HashSet<>();
static {
actions.add (ActionsManager.ACTION_KILL);
actions.add (ActionsManager.ACTION_CONTINUE);
actions.add (ActionsManager.ACTION_PAUSE);
actions.add (ActionsManager.ACTION_START);
actions.add (ActionsManager.ACTION_STEP_INTO);
actions.add (ActionsManager.ACTION_STEP_OVER);
actions.add (ActionsManager.ACTION_STEP_OUT);
actionsToDisable.addAll(actions);
// Ignore the KILL action
actionsToDisable.remove(ActionsManager.ACTION_KILL);
}
@Override
public Set getActions () {
return actions;
}
@Override
public void doAction (Object action) {
LOGGER.log(Level.FINE, "CPPLiteDebugger.doAction({0}), is kill = {1}", new Object[]{action, action == ActionsManager.ACTION_KILL});
if (action == ActionsManager.ACTION_KILL) {
finish ();
} else
if (action == ActionsManager.ACTION_CONTINUE) {
CPPThread thread = currentThread;
if (thread != null) {
thread.notifyRunning();
}
proxy.send(new Command("-exec-continue --all"));
} else
if (action == ActionsManager.ACTION_PAUSE) {
proxy.send(new Command("-exec-interrupt --all"));
} else
if (action == ActionsManager.ACTION_START) {
return ;
} else
if ( action == ActionsManager.ACTION_STEP_INTO ||
action == ActionsManager.ACTION_STEP_OUT ||
action == ActionsManager.ACTION_STEP_OVER
) {
doStep (action);
}
}
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);
}
}
@Override
public void postAction(final Object action, final Runnable actionPerformedNotifier) {
if (action == ActionsManager.ACTION_KILL) {
synchronized (CPPLiteDebugger.class) {
if (killRequestProcessor == null) {
killRequestProcessor = new RequestProcessor("CPPLite debugger finish RP", 1);
}
}
killRequestProcessor.post(new Runnable() {
@Override
public void run() {
try {
doAction(action);
} finally {
actionPerformedNotifier.run();
}
}
});
return ;
}
setDebugActionsEnabled(false);
synchronized (CPPLiteDebugger.class) {
if (actionsRequestProcessor == null) {
actionsRequestProcessor = new RequestProcessor("CPPLite debugger actions RP", 1);
}
}
actionsRequestProcessor.post(new Runnable() {
@Override
public void run() {
try {
doAction(action);
} finally {
actionPerformedNotifier.run();
setDebugActionsEnabled(true);
}
}
});
}
private void setDebugActionsEnabled(boolean enabled) {
for (Object action : actionsToDisable) {
setEnabled(action, enabled);
}
}
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.
*/
private 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));
}
}
private void finish () {
LOGGER.fine("CPPLiteDebugger.finish()");
if (finished) {
LOGGER.fine("finish(): already finished.");
return ;
}
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) {
Utils.markCurrent(new Line[] {currentLine});
Utils.showLine(new Line[] {currentLine});
}
}
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<CPPLiteDebugger, 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(debugger, 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();
}
});
}
}