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