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