blob: f39b2791fa369b46d8ed1daf6a03ae6a24736ed5 [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.php.dbgp;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.SocketException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.debugger.DebuggerEngine;
import org.netbeans.api.debugger.DebuggerManager;
import org.netbeans.api.debugger.Session;
import org.netbeans.modules.php.dbgp.breakpoints.BreakpointModel;
import org.netbeans.modules.php.dbgp.models.AbstractIDEBridge;
import org.netbeans.modules.php.dbgp.models.CallStackModel;
import org.netbeans.modules.php.dbgp.models.ThreadsModel;
import org.netbeans.modules.php.dbgp.models.VariablesModel;
import org.netbeans.modules.php.dbgp.models.WatchesModel;
import org.netbeans.modules.php.dbgp.packets.DbgpCommand;
import org.netbeans.modules.php.dbgp.packets.DbgpMessage;
import org.netbeans.modules.php.dbgp.packets.DbgpResponse;
import org.netbeans.modules.php.dbgp.packets.Error;
import org.netbeans.modules.php.dbgp.packets.InitMessage;
import org.netbeans.modules.php.dbgp.packets.Reason;
import org.netbeans.modules.php.dbgp.packets.StackGetCommand;
import org.netbeans.modules.php.dbgp.packets.Status;
import org.netbeans.modules.php.dbgp.packets.StopCommand;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
/**
* @author Radek Matous
*/
public class DebugSession extends SingleThread {
private static final Logger LOGGER = Logger.getLogger(DebugSession.class.getName());
private static final int SLEEP_TIME = 100;
private static final AtomicInteger TRANSACTION_ID = new AtomicInteger(0);
private final DebuggerOptions options;
private final BackendLauncher backendLauncher;
private final AtomicReference<Status> status;
private Session session;
private Socket sessionSocket;
private final AtomicBoolean detachRequest;
private final AtomicBoolean stopRequest;
private Thread sessionThread;
private final List<DbgpCommand> commands;
private AtomicReference<SessionId> sessionId;
private AtomicReference<DebuggerEngine> engine;
private IDESessionBridge myBridge;
private AtomicReference<String> myFileName;
private volatile boolean canceled;
DebugSession(DebuggerOptions options, BackendLauncher backendLauncher) {
commands = new LinkedList<>();
this.detachRequest = new AtomicBoolean(false);
this.stopRequest = new AtomicBoolean(false);
this.sessionId = new AtomicReference<>();
this.backendLauncher = backendLauncher;
this.status = new AtomicReference<>();
this.options = options;
}
public void startProcessing(Socket socket) {
synchronized (getSync()) {
try {
Status stat = getStatus();
detachRequest.set(true);
if (stat != null) {
waitFinished();
}
this.sessionSocket = socket;
FutureTask invokeLater = invokeLater();
invokeLater.get();
} catch (InterruptedException | ExecutionException ex) {
Exceptions.printStackTrace(ex);
}
}
}
@Override
public void run() {
preprocess();
try {
while (!detachRequest.get()) {
try {
sendCommands();
receiveData();
sleepTillNewCommand();
} catch (SocketException exc) {
log(exc);
detachRequest.set(true);
stop();
} catch (IOException e) {
log(e);
} catch (Throwable e) {
log(e, Level.SEVERE);
}
if (canceled) {
synchronized (commands) {
if (commands.isEmpty()) {
detachRequest.set(true);
stop();
}
}
}
}
} finally {
postprocess();
}
}
private void preprocess() {
detachRequest.set(false);
stopRequest.set(false);
synchronized (commands) {
commands.clear();
}
sessionId.set(null);
myBridge = new IDESessionBridge();
myFileName = new AtomicReference<>();
engine = new AtomicReference<>();
setSessionThread(Thread.currentThread());
}
private void postprocess() {
try {
getSocket().close();
} catch (IOException e) {
log(e);
} finally {
setSessionThread(null);
IDESessionBridge bridge = getBridge();
if (bridge != null) {
bridge.destroy();
}
}
}
public void initConnection(InitMessage message) {
setSessionFile(message.getFileUri());
DebuggerEngine[] engines = DebuggerManager.getDebuggerManager().getDebuggerEngines();
for (DebuggerEngine nextEngine : engines) {
SessionId id = (SessionId) nextEngine.lookupFirst(null, SessionId.class);
// [NETBEANS-5905] don't check about what the idekey is
// see also https://bugs.xdebug.org/view.php?id=2005
if (id != null && message.getSessionId() != null) {
sessionId.set(id);
id.initialize(message.getFileUri(), options.getPathMapping());
engine.set(nextEngine);
}
}
IDESessionBridge bridge = getBridge();
if (bridge != null) {
bridge.init();
}
}
private void sendCommands() throws IOException {
List<DbgpCommand> list;
synchronized (commands) {
list = new ArrayList<>(commands);
commands.clear();
}
for (DbgpCommand command : list) {
if (!detachRequest.get()) {
command.send(getSocket().getOutputStream());
if (command.wantAcknowledgment()) {
receiveData(command);
}
}
}
}
public void sendCommandLater(DbgpCommand command) {
synchronized (this) {
/*
* Do not collect command before session is not initialized.
* So any command before Init message will not be sent.
* ( F.e. commands for getting watch values will be just ignored
* if they was requested before Init message ).
*/
if (getSessionId() == null) {
return;
}
if (getSessionThread() == null) {
return;
}
addCommand(command);
}
}
public DbgpResponse sendSynchronCommand(DbgpCommand command) {
DbgpResponse retval = null;
if (canSendSynchronCommand()) {
try {
command.send(getSocket().getOutputStream());
if (command.wantAcknowledgment()) {
DbgpMessage message = receiveData(command);
if (message instanceof DbgpResponse) {
retval = (DbgpResponse) message;
}
}
} catch (SocketException e) {
log(e);
} catch (IOException e) {
log(e);
}
}
return retval;
}
private void receiveData() throws IOException {
receiveData(null);
}
private DbgpMessage receiveData(DbgpCommand command) throws IOException {
if (command != null && command.getCommand().equals(StopCommand.COMMAND)) {
detachRequest.set(true);
}
if (command != null || getSocket().getInputStream().available() > 0) {
DbgpMessage message;
try {
message = DbgpMessage.create(getSocket().getInputStream(), options.getProjectEncoding());
} catch (SocketException ex) {
if (command != null) {
LOGGER.log(Level.INFO, "COMMAND: " + command.toString() + "; TRANS_ID: " + command.getTransactionId() + "; WANT_ACK: " + command.wantAcknowledgment(), ex);
} else {
LOGGER.log(Level.INFO, "COMMAND: null", ex);
}
throw ex;
}
handleMessage(command, message);
return message;
}
return null;
}
@NbBundle.Messages({
"# {0} - Error code",
"# {1} - Error message",
"XdebugError=Response from XDebug contains errors:\n\n Code: {0}\n Message: {1}"
})
private void handleMessage(DbgpCommand command, DbgpMessage message)
throws IOException {
if (message == null) {
return;
}
if (command == null) {
// this is case when we don't need achnowl-t
message.process(this, null);
return;
}
boolean awaited = false;
if (message instanceof DbgpResponse) {
DbgpResponse response = (DbgpResponse) message;
Error error = response.getError();
if (error != null) {
String errorMessage = Bundle.XdebugError(error.getErrorCode(), error.getMessage());
DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(errorMessage, NotifyDescriptor.WARNING_MESSAGE));
LOGGER.log(Level.INFO, "PHP_XDEBUG_ERROR - code: {0}, message: {1}", new Object[]{error.getErrorCode(), error.getMessage()});
}
String id = response.getTransactionId();
if (id.equals(command.getTransactionId())) {
awaited = true;
message.process(this, command);
}
}
if (!awaited) {
message.process(this, null);
receiveData(command);
}
}
private boolean canSendSynchronCommand() {
Thread currentSessionThread = getSessionThread();
if (currentSessionThread == null) {
return false;
}
if (currentSessionThread != Thread.currentThread()) {
printing146558(Thread.currentThread());
}
return true;
}
private void printing146558(Thread currentThread) {
IllegalStateException illegalStateException = new IllegalStateException(
"Method incorrect usage. It should be called in handler thread only. " + //NOI18N
"Called from thread: " + currentThread.getName() // NOI18N
);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
illegalStateException.printStackTrace(new PrintStream(bos, false, Charset.defaultCharset().name()));
LOGGER.log(Level.WARNING, bos.toString(Charset.defaultCharset().name()));
} catch (UnsupportedEncodingException ex) {
Exceptions.printStackTrace(ex);
} finally {
try {
bos.close();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
}
@Override
public boolean cancel() {
// NETBEANS-5080 request cancellation
// startProcessing() may be called via other ways
// e.g. via command line: nc -vz localhost 9003(debugger port)
// First of all, get the socket after the above command is run
// See: Socket sessionSocket = myServer.accept(); in ServerThread.run()
// Then, invokeLater.get() is called in startProcessing()
// Finally, infinite loop occurs in run() becuase do not still receive anything
canceled = true;
return true;
}
public void processStatus(Status status, Reason reason, DbgpCommand command) {
setStatus(status);
if (status.isBreak() && reason.isOK()) {
processBreakStatus();
} else if (status.isStopping()) {
processStoppingStatus();
} else if (status.isStopped()) {
processStoppedStatus();
}
}
public void stopSession() {
sendStopCommand();
stop();
}
private void stop() {
if (!stopRequest.get()) {
stopRequest.set(true);
stopEngines();
stopBackend();
}
}
private void processBreakStatus() {
sendCommandLater(new StackGetCommand(getTransactionId()));
IDESessionBridge bridge = getBridge();
if (bridge != null) {
bridge.setSuspended(true);
ThreadsModel threadsModel = bridge.getThreadsModel();
if (threadsModel != null) {
threadsModel.updateSession(this);
}
}
}
private void processStoppingStatus() {
detachRequest.set(true);
processStoppedStatus();
}
private void processStoppedStatus() {
if (getOptions().isDebugForFirstPageOnly()) {
stop();
}
}
private void sendStopCommand() {
final boolean isDetached = detachRequest.get();
if (!isDetached) {
Thread currentThread = Thread.currentThread();
final StopCommand stopCommand = new StopCommand(getTransactionId());
if (currentThread == getSessionThread()) {
sendSynchronCommand(stopCommand);
} else {
sendCommandLater(stopCommand);
}
}
}
private void stopEngines() {
SessionManager.stopEngines(session);
}
public String getTransactionId() {
return TRANSACTION_ID.getAndIncrement() + "";
}
public SessionId getSessionId() {
return sessionId.get();
}
public IDESessionBridge getBridge() {
return myBridge;
}
public String getFileName() {
return myFileName.get();
}
private void setSessionFile(String fileName) {
myFileName.set(fileName);
}
private void sleepTillNewCommand() {
try {
// Wake up every 100 milliseconds and see if the debuggee has something to say.
// The IDE side can interrupt the sleep to send new packets to the
// debugger.
Thread.sleep(SLEEP_TIME);
} catch (InterruptedException ie) {
// OK, run the look again.
}
}
private synchronized void setSessionThread(Thread thread) {
sessionThread = thread;
}
private void warnUserInCaseOfSocketException() {
NotifyDescriptor descriptor = new NotifyDescriptor(
NbBundle.getMessage(DebugSession.class, "MSG_SocketError"),
NbBundle.getMessage(DebugSession.class, "MSG_SocketErrorTitle"),
NotifyDescriptor.OK_CANCEL_OPTION,
NotifyDescriptor.ERROR_MESSAGE,
new Object[]{NotifyDescriptor.OK_OPTION},
NotifyDescriptor.OK_OPTION);
DialogDisplayer.getDefault().notifyLater(descriptor);
}
private void addCommand(DbgpCommand command) {
synchronized (commands) {
commands.add(command);
}
}
private synchronized Thread getSessionThread() {
return sessionThread;
}
private Socket getSocket() {
return sessionSocket;
}
private void log(IOException e) {
log(e, Level.SEVERE);
}
private void log(Throwable e, Level level) {
LOGGER.log(level, null, e);
}
private void log(SocketException e) {
log(e, Level.INFO);
warnUserInCaseOfSocketException();
}
public DebuggerOptions getOptions() {
return options;
}
void startBackend() {
if (backendLauncher != null) {
backendLauncher.launch();
}
}
void stopBackend() {
if (backendLauncher != null) {
backendLauncher.stop();
}
}
/**
* @return the status
*/
public Status getStatus() {
return status.get();
}
/**
* @param status the status to set
*/
public void setStatus(Status status) {
assert status != null;
if (status == Status.BREAK) {
assert getSession() != null;
DebuggerManager.getDebuggerManager().setCurrentSession(getSession());
}
this.status.set(status);
}
/**
* @return the session
*/
public Session getSession() {
return session;
}
/**
* @param session the session to set
*/
public void setSession(Session session) {
this.session = session;
}
/*
* This class is associated with DebugSession but is intended for
* cooperation with IDE UI.
*/
public class IDESessionBridge extends AbstractIDEBridge {
@Override
protected DebuggerEngine getEngine() {
return engine.get();
}
@Override
protected DebugSession getDebugSession() {
return DebugSession.this;
}
private void init() {
hideAnnotations();
setSuspended(false);
ThreadsModel threadsModel = getThreadsModel();
if (threadsModel != null) {
threadsModel.update();
}
}
private void destroy() {
setSuspended(false);
hideAnnotations();
BreakpointModel breakpointModel = getBreakpointModel();
if (breakpointModel != null) {
breakpointModel.setCurrentStack(null, DebugSession.this);
}
CallStackModel callStackModel = getCallStackModel();
if (callStackModel != null) {
callStackModel.clearModel();
}
ThreadsModel threadsModel = getThreadsModel();
if (threadsModel != null) {
threadsModel.update();
}
VariablesModel variablesModel = getVariablesModel();
if (variablesModel != null) {
variablesModel.clearModel();
}
WatchesModel watchesModel = getWatchesModel();
if (watchesModel != null) {
watchesModel.clearModel();
}
}
}
}