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