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