PY-18029 Fix exception on attaching the debugger to a closed Python Console

This commit is contained in:
Alexander Koshevoy
2018-08-22 23:16:40 +03:00
parent 6ec3ff167f
commit c02aeb3326
4 changed files with 80 additions and 34 deletions
@@ -4,6 +4,8 @@ import com.intellij.openapi.diagnostic.Logger
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import org.apache.thrift.transport.TTransport
import org.apache.thrift.transport.TTransportException
import java.io.IOException
import java.io.PipedInputStream
import java.io.PipedOutputStream
@@ -38,7 +40,15 @@ abstract class TCumulativeTransport : TTransport() {
pipedInputStream.close()
}
final override fun read(buf: ByteArray, off: Int, len: Int): Int = pipedInputStream.read(buf, off, len)
@Throws(TTransportException::class)
final override fun read(buf: ByteArray, off: Int, len: Int): Int {
try {
return pipedInputStream.read(buf, off, len)
}
catch (e: IOException) {
throw TTransportException(TTransportException.UNKNOWN, e)
}
}
companion object {
val LOG = Logger.getInstance(TCumulativeTransport::class.java)
@@ -0,0 +1,12 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.console
/**
* The [PyConsoleProcessFinishedException] is thrown when IDE is waiting for
* the reply from Python console and the Python console process is discovered
* to be finished.
*
* @see [synchronizedPythonConsoleClient]
*/
class PyConsoleProcessFinishedException(exitValue: Int)
: RuntimeException("Console already exited with value: $exitValue while waiting for an answer.")
@@ -174,7 +174,7 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
try {
return "PyCharm".equals(myClient.handshake());
}
catch (TException e) {
catch (PyConsoleProcessFinishedException | TException e) {
throw new RuntimeException(e);
}
}
@@ -501,7 +501,7 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
try {
myClient.interrupt();
}
catch (TException e) {
catch (PyConsoleProcessFinishedException | TException e) {
LOG.error(e);
}
}
@@ -520,7 +520,6 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
public PyDebugValue evaluate(String expression, boolean execute, boolean doTrunc) throws PyDebuggerException {
if (myClient != null) {
try {
// @alexander todo add specific exception to the method (previously processed by `checkError()`)
List<DebugValue> debugValues = myClient.evaluate(expression);
return createPyDebugValue(debugValues.iterator().next(), this);
}
@@ -536,11 +535,10 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
public XValueChildrenList loadFrame() throws PyDebuggerException {
if (myClient != null) {
try {
// @alexander todo add specific exception to the method (previously processed by `checkError()`)
List<DebugValue> frame = myClient.getFrame();
return parseVars(frame, null, this);
}
catch (TException e) {
catch (PyConsoleProcessFinishedException | TException e) {
throw new PyDebuggerException("Get frame from console failed", e);
}
}
@@ -564,12 +562,11 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
final int seq = getNextFullValueSeq();
myCallbackHashMap.put(seq, pyAsyncValues);
// @alexander todo add specific exception to the method (previously processed by `checkError()`)
myClient.loadFullValue(seq, evaluationExpressions);
// previously `loadFullValue()` might return `List<PyDebugValue>` but this is no longer true
}
catch (TException e) {
catch (PyConsoleProcessFinishedException | TException e) {
for (PyAsyncValue<String> asyncValue : pyAsyncValues) {
PyDebugValue value = asyncValue.getDebugValue();
XValueNode node = value.getLastNode();
@@ -591,10 +588,12 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
public XValueChildrenList loadVariable(PyDebugValue var) throws PyDebuggerException {
if (myClient != null) {
try {
// @alexander todo add specific exception to the method (previously processed by `checkError()`)
List<DebugValue> ret = myClient.getVariable(GetVariableCommand.composeName(var));
return parseVars(ret, var, this);
}
catch (PyConsoleProcessFinishedException e) {
throw new PyDebuggerException(e.getLocalizedMessage(), e);
}
catch (TException e) {
throw new PyDebuggerException("Get variable from console failed", e);
}
@@ -619,10 +618,9 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
try {
// NOTE: The actual change is being scheduled in the exec_queue in main thread
// This method is async now
// @alexander todo add specific exception to the method (previously processed by `checkError()`)
myClient.changeVariable(variable.getEvaluationExpression(), value);
}
catch (TException e) {
catch (PyConsoleProcessFinishedException | TException e) {
throw new PyDebuggerException("Get change variable", e);
}
}
@@ -678,7 +676,7 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
// though `connectToDebugger` returns "connect complete" string, let us just ignore it
myClient.connectToDebugger(localPort, dbgOpts, extraEnvs);
}
catch (TException e) {
catch (PyConsoleProcessFinishedException | TException e) {
throw new PyDebuggerException("pydevconsole failed to execute connectToDebugger", e);
}
}
@@ -3,38 +3,34 @@
package com.jetbrains.python.console
import com.intellij.openapi.application.ApplicationManager
import com.intellij.util.ConcurrencyUtil
import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.util.concurrent.Callable
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.*
private const val PYTHON_CONSOLE_COMMAND_THREAD_FACTORY_NAME: String = "Python Console Command Executor"
@JvmOverloads
fun synchronizedPythonConsoleClient(loader: ClassLoader,
delegate: PythonConsoleBackendService.Iface,
pythonConsoleProcess: Process? = null): PythonConsoleBackendService.Iface {
val lock = ReentrantLock()
val executorService = newSingleThreadPythonConsoleCommandExecutor()
return Proxy.newProxyInstance(loader, arrayOf<Class<*>>(PythonConsoleBackendService.Iface::class.java),
InvocationHandler { _, method, args ->
val future = ApplicationManager.getApplication().executeOnPooledThread(Callable<Any> {
lock.lock()
try {
if (args == null) {
return@Callable method.invoke(delegate)
}
else {
return@Callable method.invoke(delegate, *args)
}
}
finally {
lock.unlock()
}
// we evaluate the original method in the other thread in order to control it
val future = executorService.submit(Callable {
return@Callable invokeOriginalMethod(args, method, delegate)
})
if (pythonConsoleProcess == null) {
return@InvocationHandler future.get()
try {
return@InvocationHandler future.get()
}
catch (e: ExecutionException) {
throw e.cause ?: e
}
}
while (true) {
@@ -44,10 +40,40 @@ fun synchronizedPythonConsoleClient(loader: ClassLoader,
catch (e: TimeoutException) {
if (!pythonConsoleProcess.isAlive) {
val exitValue = pythonConsoleProcess.exitValue()
throw RuntimeException(
"Console already exited with value: $exitValue while waiting for an answer.\n")
throw PyConsoleProcessFinishedException(exitValue)
}
// continue waiting for the end of the operation execution
}
catch (e: ExecutionException) {
if (!pythonConsoleProcess.isAlive) {
val exitValue = pythonConsoleProcess.exitValue()
throw PyConsoleProcessFinishedException(exitValue)
}
throw e.cause ?: e
}
}
}) as PythonConsoleBackendService.Iface
}
/**
* Creates new single thread executor with [ThreadPoolExecutor.keepAliveTime]
* equals to 60 sec.
*/
private fun newSingleThreadPythonConsoleCommandExecutor(): ExecutorService {
val threadFactory = ConcurrencyUtil.newNamedThreadFactory(PYTHON_CONSOLE_COMMAND_THREAD_FACTORY_NAME)
return ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, LinkedBlockingQueue<Runnable>(), threadFactory)
}
private fun invokeOriginalMethod(args: Array<out Any>?, method: Method, delegate: Any): Any? {
return try {
if (args != null) {
method.invoke(delegate, *args)
}
else {
method.invoke(delegate)
}
}
catch (e: InvocationTargetException) {
throw e.cause ?: e
}
}