013c2fa323af552f6a1ceb296a0a8983f330d81e
[idea/community.git] / python / src / com / jetbrains / python / console / PydevConsoleCommunication.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.console;
17
18 import com.intellij.openapi.application.AccessToken;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.fileEditor.FileEditorManager;
22 import com.intellij.openapi.progress.ProgressIndicator;
23 import com.intellij.openapi.progress.ProgressManager;
24 import com.intellij.openapi.progress.Task;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.util.Pair;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vfs.LocalFileSystem;
29 import com.intellij.openapi.vfs.VirtualFile;
30 import com.intellij.util.Function;
31 import com.intellij.xdebugger.XSourcePosition;
32 import com.intellij.xdebugger.frame.XValueChildrenList;
33 import com.jetbrains.python.console.parsing.PythonConsoleData;
34 import com.jetbrains.python.console.pydev.*;
35 import com.jetbrains.python.debugger.*;
36 import com.jetbrains.python.debugger.pydev.GetVariableCommand;
37 import com.jetbrains.python.debugger.pydev.ProtocolParser;
38 import org.apache.xmlrpc.WebServer;
39 import org.apache.xmlrpc.XmlRpcException;
40 import org.apache.xmlrpc.XmlRpcHandler;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43
44 import java.net.MalformedURLException;
45 import java.util.Collections;
46 import java.util.List;
47 import java.util.Vector;
48
49 /**
50  * Communication with Xml-rpc with the client.
51  *
52  * @author Fabio
53  */
54 public class PydevConsoleCommunication extends AbstractConsoleCommunication implements XmlRpcHandler,
55                                                                                        PyFrameAccessor {
56
57   private static final String EXEC_LINE = "execLine";
58   private static final String EXEC_MULTILINE = "execMultipleLines";
59   private static final String GET_COMPLETIONS = "getCompletions";
60   private static final String GET_DESCRIPTION = "getDescription";
61   private static final String GET_FRAME = "getFrame";
62   private static final String GET_VARIABLE = "getVariable";
63   private static final String CHANGE_VARIABLE = "changeVariable";
64   private static final String CONNECT_TO_DEBUGGER = "connectToDebugger";
65   private static final String HANDSHAKE = "handshake";
66   private static final String CLOSE = "close";
67   private static final String EVALUATE = "evaluate";
68   private static final String GET_ARRAY = "getArray";
69
70   /**
71    * XML-RPC client for sending messages to the server.
72    */
73   private IPydevXmlRpcClient myClient;
74
75   /**
76    * This is the server responsible for giving input to a raw_input() requested.
77    */
78   private WebServer myWebServer;
79
80   private static final Logger LOG = Logger.getInstance(PydevConsoleCommunication.class.getName());
81
82   /**
83    * Input that should be sent to the server (waiting for raw_input)
84    */
85   protected volatile String inputReceived;
86   /**
87    * Response that should be sent back to the shell.
88    */
89   protected volatile InterpreterResponse nextResponse;
90   /**
91    * Helper to keep on busy loop.
92    */
93   private volatile Object lock2 = new Object();
94   /**
95    * Keeps a flag indicating that we were able to communicate successfully with the shell at least once
96    * (if we haven't we may retry more than once the first time, as jython can take a while to initialize
97    * the communication)
98    */
99   private volatile boolean firstCommWorked = false;
100
101   private boolean myExecuting;
102   private PythonDebugConsoleCommunication myDebugCommunication;
103
104   /**
105    * Initializes the xml-rpc communication.
106    *
107    * @param port    the port where the communication should happen.
108    * @param process this is the process that was spawned (server for the XML-RPC)
109    * @throws MalformedURLException
110    */
111   public PydevConsoleCommunication(Project project, int port, Process process, int clientPort) throws Exception {
112     super(project);
113
114     //start the server that'll handle input requests
115     myWebServer = new WebServer(clientPort, null);
116     myWebServer.addHandler("$default", this);
117     this.myWebServer.start();
118
119     this.myClient = new PydevXmlRpcClient(process, port);
120   }
121
122   public boolean handshake() throws XmlRpcException {
123     if (myClient != null) {
124       Object ret = myClient.execute(HANDSHAKE, new Object[]{});
125       if (ret instanceof String) {
126         String retVal = (String)ret;
127         return "PyCharm".equals(retVal);
128       }
129     }
130     return false;
131   }
132
133   /**
134    * Stops the communication with the client (passes message for it to quit).
135    */
136   public synchronized void close() {
137     if (this.myClient != null) {
138       new Task.Backgroundable(myProject, "Close console communication", true) {
139         @Override
140         public void run(@NotNull ProgressIndicator indicator) {
141           try {
142             PydevConsoleCommunication.this.myClient.execute(CLOSE, new Object[0]);
143           }
144           catch (Exception e) {
145             //Ok, we can ignore this one on close.
146           }
147           PydevConsoleCommunication.this.myClient = null;
148         }
149       }.queue();
150     }
151
152     if (myWebServer != null) {
153       myWebServer.shutdown();
154       myWebServer = null;
155     }
156   }
157
158   /**
159    * Variables that control when we're expecting to give some input to the server or when we're
160    * adding some line to be executed
161    */
162
163   /**
164    * Helper to keep on busy loop.
165    */
166   private volatile Object lock = new Object();
167
168
169   /**
170    * Called when the server is requesting some input from this class.
171    */
172   public Object execute(String method, Vector params) throws Exception {
173     if ("NotifyFinished".equals(method)) {
174       return execNotifyFinished((Boolean)params.get(0));
175     }
176     else if ("RequestInput".equals(method)) {
177       return execRequestInput();
178     }
179     else if ("IPythonEditor".equals(method)) {
180       return execIPythonEditor(params);
181     }
182     else if ("NotifyAboutMagic".equals(method)) {
183       return execNotifyAboutMagic(params);
184     }
185     else {
186       throw new UnsupportedOperationException();
187     }
188   }
189
190   private Object execNotifyAboutMagic(Vector params) {
191     List<String> commands = (List<String>)params.get(0);
192     boolean isAutoMagic = (Boolean)params.get(1);
193
194     if (getConsoleFile() != null) {
195       PythonConsoleData consoleData = PyConsoleUtil.getOrCreateIPythonData(getConsoleFile());
196       consoleData.setIPythonAutomagic(isAutoMagic);
197       consoleData.setIPythonMagicCommands(commands);
198     }
199
200     return "";
201   }
202
203   private Object execIPythonEditor(Vector params) {
204
205     String path = (String)params.get(0);
206     int line = Integer.parseInt((String)params.get(1));
207
208     final VirtualFile file = StringUtil.isEmpty(path) ? null : LocalFileSystem.getInstance().findFileByPath(path);
209     if (file != null) {
210       ApplicationManager.getApplication().invokeLater(new Runnable() {
211         @Override
212         public void run() {
213           AccessToken at = ApplicationManager.getApplication().acquireReadActionLock();
214
215           try {
216             FileEditorManager.getInstance(myProject).openFile(file, true);
217           }
218           finally {
219             at.finish();
220           }
221         }
222       });
223
224       return Boolean.TRUE;
225     }
226
227     return Boolean.FALSE;
228   }
229
230   private Object execNotifyFinished(boolean more) {
231     setExecuting(false);
232     notifyCommandExecuted(more);
233     return true;
234   }
235
236   private void setExecuting(boolean executing) {
237     myExecuting = executing;
238   }
239
240   private Object execRequestInput() {
241     waitingForInput = true;
242     inputReceived = null;
243     boolean needInput = true;
244
245     //let the busy loop from execInterpreter free and enter a busy loop
246     //in this function until execInterpreter gives us an input
247     nextResponse = new InterpreterResponse(false, needInput);
248
249     notifyInputRequested();
250
251     //busy loop until we have an input
252     while (inputReceived == null) {
253       synchronized (lock) {
254         try {
255           lock.wait(10);
256         }
257         catch (InterruptedException e) {
258           //pass
259         }
260       }
261     }
262     return inputReceived;
263   }
264
265   /**
266    * Executes the needed command
267    *
268    * @param command
269    * @return a Pair with (null, more) or (error, false)
270    * @throws XmlRpcException
271    */
272   protected Pair<String, Boolean> exec(final ConsoleCodeFragment command) throws XmlRpcException {
273     setExecuting(true);
274     Object execute = myClient.execute(command.isSingleLine() ? EXEC_LINE : EXEC_MULTILINE, new Object[]{command.getText()});
275
276     Object object;
277     if (execute instanceof Vector) {
278       object = ((Vector)execute).get(0);
279     }
280     else if (execute.getClass().isArray()) {
281       object = ((Object[])execute)[0];
282     }
283     else {
284       object = execute;
285     }
286     Pair<String, Boolean> result = parseResult(object);
287     if (result.second) {
288       setExecuting(false);
289     }
290
291     return result;
292   }
293
294   private Pair<String, Boolean> parseResult(Object object) {
295     if (object instanceof Boolean) {
296       return new Pair<String, Boolean>(null, (Boolean)object);
297     }
298     else {
299       return parseExecResponseString(object.toString());
300     }
301   }
302
303   /**
304    * @return completions from the client
305    */
306   @NotNull
307   public List<PydevCompletionVariant> getCompletions(String text, String actTok) throws Exception {
308     if (myDebugCommunication != null && myDebugCommunication.isSuspended()) {
309       return myDebugCommunication.getCompletions(text, actTok);
310     }
311
312     if (waitingForInput) {
313       return Collections.emptyList();
314     }
315     final Object fromServer = myClient.execute(GET_COMPLETIONS, new Object[]{text, actTok});
316
317     return PydevXmlUtils.decodeCompletions(fromServer, actTok);
318   }
319
320   /**
321    * @return the description of the given attribute in the shell
322    */
323   public String getDescription(String text) throws Exception {
324     if (myDebugCommunication != null && myDebugCommunication.isSuspended()) {
325       return myDebugCommunication.getDescription(text);
326     }
327     if (waitingForInput) {
328       return "Unable to get description: waiting for input.";
329     }
330     return myClient.execute(GET_DESCRIPTION, new Object[]{text}).toString();
331   }
332
333   /**
334    * Executes a given line in the interpreter.
335    *
336    * @param command the command to be executed in the client
337    */
338   public void execInterpreter(final ConsoleCodeFragment command, final Function<InterpreterResponse, Object> onResponseReceived) {
339     if (myDebugCommunication != null && myDebugCommunication.isSuspended()) {
340       myDebugCommunication.execInterpreter(command, onResponseReceived);
341       return; //TODO: handle text input and other cases
342     }
343     nextResponse = null;
344     if (waitingForInput) {
345       inputReceived = command.getText();
346       waitingForInput = false;
347       //the thread that we started in the last exec is still alive if we were waiting for an input.
348     }
349     else {
350       //create a thread that'll keep locked until an answer is received from the server.
351       new Task.Backgroundable(myProject, "REPL Communication", true) {
352
353         @Override
354         public void run(@NotNull ProgressIndicator indicator) {
355           boolean needInput = false;
356           try {
357
358             Pair<String, Boolean> executed = null;
359
360             //the 1st time we'll do a connection attempt, we can try to connect n times (until the 1st time the connection
361             //is accepted) -- that's mostly because the server may take a while to get started.
362             int commAttempts = 0;
363             while (true) {
364               if (indicator.isCanceled()) {
365                 return;
366               }
367
368               executed = exec(command);
369
370               //executed.o1 is not null only if we had an error
371
372               String refusedConnPattern = "Failed to read servers response";
373               // Was "refused", but it didn't
374               // work on non English system
375               // (in Spanish localized systems
376               // it is "rechazada")
377               // This string always works,
378               // because it is hard-coded in
379               // the XML-RPC library)
380               if (executed.first != null && executed.first.indexOf(refusedConnPattern) != -1) {
381                 if (firstCommWorked) {
382                   break;
383                 }
384                 else {
385                   if (commAttempts < MAX_ATTEMPTS) {
386                     commAttempts += 1;
387                     Thread.sleep(250);
388                     executed = Pair.create("", executed.second);
389                   }
390                   else {
391                     break;
392                   }
393                 }
394               }
395               else {
396                 break;
397               }
398
399               //unreachable code!! -- commented because eclipse will complain about it
400               //throw new RuntimeException("Can never get here!");
401             }
402
403             firstCommWorked = true;
404
405             boolean more = executed.second;
406
407             nextResponse = new InterpreterResponse(more, needInput);
408           }
409           catch (Exception e) {
410             nextResponse = new InterpreterResponse(false, needInput);
411           }
412         }
413       }.queue();
414
415
416       //busy loop waiting for the answer (or having the console die).
417       ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
418         @Override
419         public void run() {
420           final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
421           progressIndicator.setText("Waiting for REPL response with " + (int)(TIMEOUT / 10e8) + "s timeout");
422           final long startTime = System.nanoTime();
423           while (nextResponse == null) {
424             if (progressIndicator.isCanceled()) {
425               LOG.debug("Canceled");
426               nextResponse = new InterpreterResponse(false, false);
427             }
428
429             final long time = System.nanoTime() - startTime;
430             progressIndicator.setFraction(((double)time) / TIMEOUT);
431             if (time > TIMEOUT) {
432               LOG.debug("Timeout exceeded");
433               nextResponse = new InterpreterResponse(false, false);
434             }
435             synchronized (lock2) {
436               try {
437                 lock2.wait(20);
438               }
439               catch (InterruptedException e) {
440                 LOG.error(e);
441               }
442             }
443           }
444           onResponseReceived.fun(nextResponse);
445         }
446       }, "Waiting for REPL response", true, myProject);
447     }
448   }
449
450   @Override
451   public void interrupt() {
452     try {
453       myClient.execute("interrupt", new Object[]{});
454     }
455     catch (XmlRpcException e) {
456       LOG.error(e);
457     }
458   }
459
460   @Override
461   public boolean isExecuting() {
462     return myExecuting;
463   }
464
465   @Override
466   public PyDebugValue evaluate(String expression, boolean execute, boolean doTrunc) throws PyDebuggerException {
467     if (myClient != null) {
468       try {
469         Object ret = myClient.execute(EVALUATE, new Object[]{expression});
470         if (ret instanceof String) {
471           return ProtocolParser.parseValue((String)ret, this);
472         }
473         else {
474           checkError(ret);
475         }
476       }
477       catch (Exception e) {
478         throw new PyDebuggerException("Evaluate in console failed", e);
479       }
480     }
481     return null;
482   }
483
484   @Nullable
485   @Override
486   public XValueChildrenList loadFrame() throws PyDebuggerException {
487     if (myClient != null) {
488       try {
489         Object ret = myClient.execute(GET_FRAME, new Object[]{});
490         if (ret instanceof String) {
491           return parseVars((String)ret, null);
492         }
493         else {
494           checkError(ret);
495         }
496       }
497       catch (XmlRpcException e) {
498         throw new PyDebuggerException("Get frame from console failed", e);
499       }
500     }
501     return new XValueChildrenList();
502   }
503
504   private XValueChildrenList parseVars(String ret, PyDebugValue parent) throws PyDebuggerException {
505     final List<PyDebugValue> values = ProtocolParser.parseValues(ret, this);
506     XValueChildrenList list = new XValueChildrenList(values.size());
507     for (PyDebugValue v : values) {
508       list.add(v.getName(), parent != null ? v.setParent(parent) : v);
509     }
510     return list;
511   }
512
513   @Override
514   public XValueChildrenList loadVariable(PyDebugValue var) throws PyDebuggerException {
515     if (myClient != null) {
516       try {
517         Object ret = myClient.execute(GET_VARIABLE, new Object[]{GetVariableCommand.composeName(var)});
518         if (ret instanceof String) {
519           return parseVars((String)ret, var);
520         }
521         else {
522           checkError(ret);
523         }
524       }
525       catch (XmlRpcException e) {
526         throw new PyDebuggerException("Get variable from console failed", e);
527       }
528     }
529     return new XValueChildrenList();
530   }
531
532   @Override
533   public void changeVariable(PyDebugValue variable, String value) throws PyDebuggerException {
534     if (myClient != null) {
535       try {
536         // NOTE: The actual change is being scheduled in the exec_queue in main thread
537         // This method is async now
538         Object ret = myClient.execute(CHANGE_VARIABLE, new Object[]{variable.getEvaluationExpression(), value});
539         checkError(ret);
540       }
541       catch (XmlRpcException e) {
542         throw new PyDebuggerException("Get change variable", e);
543       }
544     }
545   }
546
547   @Nullable
548   @Override
549   public PyReferrersLoader getReferrersLoader() {
550     return null;
551   }
552
553   @Override
554   public ArrayChunk getArrayItems(PyDebugValue var, int rowOffset, int colOffset, int rows, int cols, String format)
555     throws PyDebuggerException {
556     if (myClient != null) {
557       try {
558         Object ret = myClient.execute(GET_ARRAY, new Object[]{var.getName(), rowOffset, colOffset, rows, cols, format});
559         if (ret instanceof String) {
560           return ProtocolParser.parseArrayValues((String)ret, this);
561         }
562         else {
563           checkError(ret);
564         }
565       }
566       catch (Exception e) {
567         throw new PyDebuggerException("Evaluate in console failed", e);
568       }
569     }
570     return null;
571   }
572
573   @Nullable
574   @Override
575   public XSourcePosition getSourcePositionForName(String name) {
576     return null;
577   }
578
579   @Nullable
580   @Override
581   public XSourcePosition getSourcePositionForType(String type) {
582     return null;
583   }
584
585   /**
586    * Request that pydevconsole connect (with pydevd) to the specified port
587    *
588    * @param localPort port for pydevd to connect to.
589    * @throws Exception if connection fails
590    */
591   public void connectToDebugger(int localPort) throws Exception {
592     if (waitingForInput) {
593       throw new Exception("Can't connect debugger now, waiting for input");
594     }
595     Object result = myClient.execute(CONNECT_TO_DEBUGGER, new Object[]{localPort});
596     Exception exception = null;
597     if (result instanceof Vector) {
598       Vector resultarray = (Vector)result;
599       if (resultarray.size() == 1) {
600         if ("connect complete".equals(resultarray.get(0))) {
601           return;
602         }
603         if (resultarray.get(0) instanceof String) {
604           exception = new Exception((String)resultarray.get(0));
605         }
606         if (resultarray.get(0) instanceof Exception) {
607           exception = (Exception)resultarray.get(0);
608         }
609       }
610     }
611     throw new PyDebuggerException("pydevconsole failed to execute connectToDebugger", exception);
612   }
613
614   private static void checkError(Object ret) throws PyDebuggerException {
615     if (ret instanceof Object[] && ((Object[])ret).length == 1) {
616       throw new PyDebuggerException(((Object[])ret)[0].toString());
617     }
618   }
619
620   public void setDebugCommunication(PythonDebugConsoleCommunication debugCommunication) {
621     myDebugCommunication = debugCommunication;
622   }
623
624   public PythonDebugConsoleCommunication getDebugCommunication() {
625     return myDebugCommunication;
626   }
627 }