JAL-3551 JAL-3931 view pymol commands/responses in Log.trace
[jalview.git] / src / jalview / ext / pymol / PymolManager.java
1 package jalview.ext.pymol;
2
3 import java.io.BufferedReader;
4 import java.io.File;
5 import java.io.IOException;
6 import java.io.InputStream;
7 import java.io.InputStreamReader;
8 import java.io.PrintWriter;
9 import java.net.HttpURLConnection;
10 import java.net.SocketException;
11 import java.net.URL;
12 import java.nio.file.Paths;
13 import java.util.ArrayList;
14 import java.util.List;
15
16 import jalview.bin.Cache;
17 import jalview.gui.Preferences;
18 import jalview.structure.StructureCommand;
19 import jalview.structure.StructureCommandI;
20
21 public class PymolManager
22 {
23   private static final int RPC_REPLY_TIMEOUT_MS = 15000;
24
25   private static final int CONNECTION_TIMEOUT_MS = 100;
26
27   private static final String POST1 = "<methodCall><methodName>";
28
29   private static final String POST2 = "</methodName><params>";
30
31   private static final String POST3 = "</params></methodCall>";
32
33   private Process pymolProcess;
34
35   private int pymolXmlRpcPort;
36
37   /**
38    * Returns a list of paths to try for the PyMOL executable. Any user
39    * preference is placed first, otherwise 'standard' paths depending on the
40    * operating system.
41    * 
42    * @return
43    */
44   public static List<String> getPymolPaths()
45   {
46     return getPymolPaths(System.getProperty("os.name"));
47   }
48
49   /**
50    * Returns a list of paths to try for the PyMOL executable. Any user
51    * preference is placed first, otherwise 'standard' paths depending on the
52    * operating system.
53    * 
54    * @param os
55    *          operating system as reported by environment variable
56    *          {@code os.name}
57    * @return
58    */
59   protected static List<String> getPymolPaths(String os)
60   {
61     List<String> pathList = new ArrayList<>();
62   
63     String userPath = Cache
64             .getDefault(Preferences.PYMOL_PATH, null);
65     if (userPath != null)
66     {
67       pathList.add(userPath);
68     }
69   
70     /*
71      * add default installation paths
72      */
73     String pymol = "PyMOL";
74     if (os.startsWith("Linux"))
75     {
76       pathList.add("/usr/local/pymol/bin/" + pymol);
77       pathList.add("/usr/local/bin/" + pymol);
78       pathList.add("/usr/bin/" + pymol);
79       pathList.add(System.getProperty("user.home") + "/opt/bin/" + pymol);
80     }
81     else if (os.startsWith("Windows"))
82     {
83       // todo Windows installation path(s)
84     }
85     else if (os.startsWith("Mac"))
86     {
87       pathList.add("/Applications/PyMOL.app/Contents/MacOS/" + pymol);
88     }
89     return pathList;
90   }
91
92   public boolean isPymolLaunched()
93   {
94     // TODO pull up generic methods for external viewer processes
95     boolean launched = false;
96     if (pymolProcess != null)
97     {
98       try
99       {
100         pymolProcess.exitValue();
101         // if we get here, process has ended
102       } catch (IllegalThreadStateException e)
103       {
104         // ok - not yet terminated
105         launched = true;
106       }
107     }
108     return launched;
109   }
110
111   /**
112    * Sends the command to Pymol; if requested, tries to get and return any
113    * replies, else returns null
114    * 
115    * @param command
116    * @param getReply
117    * @return
118    */
119   public List<String> sendCommand(StructureCommandI command,
120           boolean getReply)
121   {
122     String postBody = getPostRequest(command);
123     // System.out.println(postBody);// debug
124     String rpcUrl = "http://127.0.0.1:" + this.pymolXmlRpcPort;
125     PrintWriter out = null;
126     BufferedReader in = null;
127     List<String> result = getReply ? new ArrayList<>() : null;
128
129     try
130     {
131       URL realUrl = new URL(rpcUrl);
132       HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
133       conn.setRequestProperty("accept", "*/*");
134       conn.setRequestProperty("content-type", "text/xml");
135       conn.setDoOutput(true);
136       conn.setDoInput(true);
137       out = new PrintWriter(conn.getOutputStream());
138       out.print(postBody);
139       out.flush();
140       int rc = conn.getResponseCode();
141       if (rc != HttpURLConnection.HTTP_OK)
142       {
143         Cache.log.error(
144                 String.format("Error status from %s: %d", rpcUrl, rc));
145         return result;
146       }
147
148       InputStream inputStream = conn.getInputStream();
149       if (getReply)
150       {
151         in = new BufferedReader(new InputStreamReader(inputStream));
152         String line;
153         while ((line = in.readLine()) != null)
154         {
155           result.add(line);
156         }
157       }
158     } catch (SocketException e)
159     {
160       // thrown when 'quit' command is sent to PyMol
161       Cache.log.warn(String.format("Request to %s returned %s", rpcUrl,
162               e.toString()));
163     } catch (Exception e)
164     {
165       e.printStackTrace();
166     } finally
167     {
168       if (out != null)
169       {
170         out.close();
171       }
172       if (Cache.log.isTraceEnabled())
173       {
174         Cache.log.trace("Sent: " + command.toString());
175         if (result != null)
176         {
177           Cache.log.trace("Received: " + result);
178         }
179       }
180     }
181     return result;
182   }
183
184   /**
185    * Builds the body of the XML-RPC format POST request to execute the command
186    * 
187    * @param command
188    * @return
189    */
190   static String getPostRequest(StructureCommandI command)
191   {
192     StringBuilder sb = new StringBuilder(64);
193     sb.append(POST1).append(command.getCommand()).append(POST2);
194     if (command.hasParameters())
195     {
196       for (String p : command.getParameters())
197       {
198         /*
199          * for now assuming all are string - <string> element is optional
200          * refactor in future if other data types needed
201          * https://www.tutorialspoint.com/xml-rpc/xml_rpc_data_model.htm
202          */
203         sb.append("<parameter><value>").append(p)
204                 .append("</value></parameter>");
205       }
206     }
207     sb.append(POST3);
208     return sb.toString();
209   }
210
211   public Process launchPymol()
212   {
213     // todo pull up much of this
214     // Do nothing if already launched
215     if (isPymolLaunched())
216     {
217       return pymolProcess;
218     }
219
220     String error = "Error message: ";
221     for (String pymolPath : getPymolPaths())
222     {
223       try
224       {
225         // ensure symbolic links are resolved
226         pymolPath = Paths.get(pymolPath).toRealPath().toString();
227         File path = new File(pymolPath);
228         // uncomment the next line to simulate Pymol not installed
229         // path = new File(pymolPath + "x");
230         if (!path.canExecute())
231         {
232           error += "File '" + path + "' does not exist.\n";
233           continue;
234         }
235         List<String> args = new ArrayList<>();
236         args.add(pymolPath);
237         args.add("-R"); // https://pymolwiki.org/index.php/RPC
238         ProcessBuilder pb = new ProcessBuilder(args);
239         pymolProcess = pb.start();
240         error = "";
241         break;
242       } catch (Exception e)
243       {
244         // Pymol could not be started using this path
245         error += e.getMessage();
246       }
247     }
248
249     if (pymolProcess != null)
250     {
251       this.pymolXmlRpcPort = getPortNumber();
252       if (pymolXmlRpcPort > 0)
253       {
254         Cache.log.info("PyMOL XMLRPC started on port " + pymolXmlRpcPort);
255       }
256       else
257       {
258         error += "Failed to read PyMOL XMLRPC port number";
259         Cache.log.error(error);
260         pymolProcess.destroy();
261         pymolProcess = null;
262       }
263     }
264
265     return pymolProcess;
266   }
267
268   private int getPortNumber()
269   {
270     // TODO pull up most of this!
271     int port = 0;
272     InputStream readChan = pymolProcess.getInputStream();
273     BufferedReader lineReader = new BufferedReader(
274             new InputStreamReader(readChan));
275     StringBuilder responses = new StringBuilder();
276     try
277     {
278       String response = lineReader.readLine();
279       while (response != null)
280       {
281         responses.append("\n" + response);
282         // expect: xml-rpc server running on host localhost, port 9123
283         if (response.contains("xml-rpc"))
284         {
285           String[] tokens = response.split(" ");
286           for (int i = 0; i < tokens.length - 1; i++)
287           {
288             if ("port".equals(tokens[i]))
289             {
290               port = Integer.parseInt(tokens[i + 1]);
291               break;
292             }
293           }
294         }
295         if (port > 0)
296         {
297           break; // hack for hanging readLine()
298         }
299         response = lineReader.readLine();
300       }
301     } catch (Exception e)
302     {
303       Cache.log.error("Failed to get REST port number from " + responses
304               + ": " + e.getMessage());
305       // logger.error("Failed to get REST port number from " + responses + ": "
306       // + e.getMessage());
307     } finally
308     {
309       try
310       {
311         lineReader.close();
312       } catch (IOException e2)
313       {
314       }
315     }
316     if (port == 0)
317     {
318       Cache.log.error("Failed to start PyMOL with XMLRPC, response was: "
319               + responses);
320     }
321     Cache.log.error("PyMOL started with XMLRPC on port " + port);
322     return port;
323   }
324
325 }