Fix PY-20831: Do not let console lock up UI thread for a long time

This commit is contained in:
fitermay
2016-09-17 21:01:06 -04:00
parent 5767661445
commit 5aa91a6bab
6 changed files with 113 additions and 107 deletions
@@ -19,4 +19,7 @@ public interface IPydevXmlRpcClient {
* @throws XmlRpcException
*/
Object execute(String command, Object[] args) throws XmlRpcException;
Object execute(String command, Object[] args, long timeoutMillis) throws XmlRpcException;
}
@@ -1,102 +0,0 @@
package com.jetbrains.python.console.pydev;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.net.NetUtils;
import org.apache.xmlrpc.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Vector;
/**
* Subclass of XmlRpcClient that will monitor the process so that if the process is destroyed, we stop waiting
* for messages from it.
*
* @author Fabio
*/
public class PydevXmlRpcClient implements IPydevXmlRpcClient {
/**
* Internal xml-rpc client (responsible for the actual communication with the server)
*/
private final XmlRpcClient impl;
/**
* The process where the server is being executed.
*/
private final Process process;
/**
* ItelliJ Logging
*/
private static final Logger LOG = Logger.getInstance(PydevXmlRpcClient.class.getName());
private static final long TIME_LIMIT = 60000;
/**
* Constructor (see fields description)
*/
public PydevXmlRpcClient(Process process, int port) throws MalformedURLException {
XmlRpc.setDefaultInputEncoding("UTF8"); //even though it uses UTF anyway
impl = new XmlRpcClientLite(NetUtils.getLocalHostString(), port);
//this.impl = new XmlRpcClient(url, new CommonsXmlRpcTransportFactory(url));
this.process = process;
}
/**
* Executes a command in the server.
* <p/>
* Within this method, we should be careful about being able to return if the server dies.
* If we wanted to have a timeout, this would be the place to add it.
*
* @return the result from executing the given command in the server.
*/
@Override
public Object execute(String command, Object[] args) throws XmlRpcException {
final Object[] result = new Object[]{null};
//make an async call so that we can keep track of not actually having an answer.
impl.executeAsync(command, new Vector(Arrays.asList(args)), new AsyncCallback() {
@Override
public void handleError(Exception error, URL url, String method) {
result[0] = new Object[]{error.getMessage()};
}
@Override
public void handleResult(Object recievedResult, URL url, String method) {
result[0] = recievedResult;
}
});
long started = System.currentTimeMillis();
//busy loop waiting for the answer (or having the console die).
while (result[0] == null && System.currentTimeMillis() - started < TIME_LIMIT) {
try {
if (process != null) {
int exitValue = process.exitValue();
result[0] = new Object[]{String.format("Console already exited with value: %s while waiting for an answer.\n", exitValue)};
//ok, we have an exit value!
break;
}
}
catch (IllegalThreadStateException e) {
//that's ok... let's sleep a bit
synchronized (this) {
try {
wait(10);
}
catch (InterruptedException e1) {
LOG.error(e1);
}
}
}
}
if (result[0] == null) {
throw new XmlRpcException(-1, "Timeout while connecting to server");
}
return result[0];
}
}
@@ -0,0 +1,101 @@
package com.jetbrains.python.console.pydev
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.util.net.NetUtils
import org.apache.xmlrpc.*
import java.net.MalformedURLException
import java.net.URL
import java.util.Arrays
import java.util.Vector
import java.util.concurrent.TimeUnit
/**
* Subclass of XmlRpcClient that will monitor the process so that if the process is destroyed, we stop waiting
* for messages from it.
* @author Fabio
*/
class PydevXmlRpcClient
/**
* Constructor (see fields description)
*/
@Throws(MalformedURLException::class)
constructor(private val process: Process, port: Int) : IPydevXmlRpcClient {
/**
* Internal xml-rpc client (responsible for the actual communication with the server)
*/
private val impl: XmlRpcClient
init {
XmlRpc.setDefaultInputEncoding("UTF8") //even though it uses UTF anyway
impl = XmlRpcClientLite(NetUtils.getLocalHostString(), port)
}
override fun execute(command: String, args: Array<Any>): Any {
return execute(command, args, TIME_LIMIT)
}
/**
* Executes a command in the server.
*
*
* Within this method, we should be careful about being able to return if the server dies.
* If we wanted to have a timeout, this would be the place to add it.
* @return the result from executing the given command in the server.
*/
@Throws(XmlRpcException::class)
override fun execute(command: String, args: Array<Any>, timeoutMillis: Long): Any {
val result = arrayOf<Any?>(null)
//make an async call so that we can keep track of not actually having an answer.
impl.executeAsync(command, Vector(Arrays.asList(*args)), object : AsyncCallback {
override fun handleError(error: Exception, url: URL, method: String) {
result[0] = makeError(error.message ?: "Unknown Error")
}
override fun handleResult(recievedResult: Any, url: URL, method: String) {
result[0] = recievedResult
}
})
val started = System.currentTimeMillis()
val progress = ProgressManager.getInstance().progressIndicator
//busy loop waiting for the answer (or having the console die).
while (result[0] == null && System.currentTimeMillis() - started < TIME_LIMIT) {
progress?.let {
progress.checkCanceled()
}
val exitValue = process.waitFor(10, TimeUnit.MILLISECONDS)
if (exitValue) {
result[0] = makeError(String.format("Console already exited with value: %s while waiting for an answer.\n", exitValue))
break
}
}
return result[0] ?: throw XmlRpcException(-1, "Timeout while connecting to server")
}
fun makeError(error: String): Array<Any> {
return arrayOf(error)
}
companion object {
/**
* ItelliJ Logging
*/
private val LOG = Logger.getInstance(PydevXmlRpcClient::class.java.name)
private val TIME_LIMIT: Long = 60000
}
}
+2 -2
View File
@@ -12,6 +12,6 @@
<orderEntry type="library" name="Guava" level="project" />
<orderEntry type="library" name="xpp3-1.1.4-min" level="project" />
<orderEntry type="module" module-name="util" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
</component>
</module>
</module>
@@ -137,7 +137,7 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
*/
public synchronized void close() {
if (this.myClient != null) {
new Task.Backgroundable(myProject, "Close console communication", true) {
new Task.Backgroundable(myProject, "Close Console Communication", true) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
try {
@@ -326,7 +326,7 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
if (waitingForInput) {
return "Unable to get description: waiting for input.";
}
return myClient.execute(GET_DESCRIPTION, new Object[]{text}).toString();
return myClient.execute(GET_DESCRIPTION, new Object[]{text}, 5000).toString();
}
/**
@@ -913,8 +913,12 @@ public class PydevConsoleRunnerImpl implements PydevConsoleRunner {
@Override
public void run(@NotNull ProgressIndicator indicator) {
if (myProcessHandler != null) {
UIUtil.invokeLaterIfNeeded(() -> closeCommunication());
UIUtil.invokeAndWaitIfNeeded((Runnable)() -> closeCommunication());
boolean processStopped = myProcessHandler.waitFor(5000L);
if (!processStopped && myProcessHandler.canKillProcess()) {
myProcessHandler.killProcess();
}
myProcessHandler.waitFor();
}