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