2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 package jalview.ext.pymol;
23 import java.io.BufferedReader;
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;
32 import java.nio.file.Paths;
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.Locale;
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;
43 public class PymolManager
45 private static final int RPC_REPLY_TIMEOUT_MS = 15000;
47 private static final int CONNECTION_TIMEOUT_MS = 100;
49 private static final String POST1 = "<methodCall><methodName>";
51 private static final String POST2 = "</methodName><params>";
53 private static final String POST3 = "</params></methodCall>";
55 private Process pymolProcess;
57 private int pymolXmlRpcPort;
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
66 public static List<String> getPymolPaths()
68 return getPymolPaths(System.getProperty("os.name"));
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
77 * operating system as reported by environment variable
81 protected static List<String> getPymolPaths(String os)
83 List<String> pathList = new ArrayList<>();
85 String userPath = Cache.getDefault(Preferences.PYMOL_PATH, null);
88 pathList.add(userPath);
92 * add default installation paths
94 String pymol = "PyMOL";
95 if (os.startsWith("Linux"))
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);
102 else if (os.startsWith("Windows"))
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)" })
113 for (String path : new String[] { "Schrodinger\\PyMOL2", "PyMOL" })
115 for (String binary : new String[] { "PyMOLWinWithConsole.bat",
116 "Scripts\\pymol.exe", "PyMOLWin.exe" })
118 pathList.add(String.format("%s\\%s\\%s", root, path, binary));
123 else if (os.startsWith("Mac"))
125 pathList.add("/Applications/PyMOL.app/Contents/MacOS/" + pymol);
130 public boolean isPymolLaunched()
132 // TODO pull up generic methods for external viewer processes
133 boolean launched = false;
134 if (pymolProcess != null)
138 pymolProcess.exitValue();
139 // if we get here, process has ended
140 } catch (IllegalThreadStateException e)
142 // ok - not yet terminated
150 * Sends the command to Pymol; if requested, tries to get and return any
151 * replies, else returns null
157 public List<String> sendCommand(StructureCommandI command,
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;
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());
178 int rc = conn.getResponseCode();
179 if (rc != HttpURLConnection.HTTP_OK)
182 String.format("Error status from %s: %d", rpcUrl, rc));
186 InputStream inputStream = conn.getInputStream();
189 in = new BufferedReader(new InputStreamReader(inputStream));
191 while ((line = in.readLine()) != null)
196 } catch (SocketException e)
198 // thrown when 'quit' command is sent to PyMol
199 Console.warn(String.format("Request to %s returned %s", rpcUrl,
201 } catch (Exception e)
210 if (Console.isTraceEnabled())
212 Console.trace("Sent: " + command.toString());
215 Console.trace("Received: " + result);
223 * Builds the body of the XML-RPC format POST request to execute the command
228 static String getPostRequest(StructureCommandI command)
230 StringBuilder sb = new StringBuilder(64);
231 sb.append(POST1).append(command.getCommand()).append(POST2);
232 if (command.hasParameters())
234 for (String p : command.getParameters())
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
241 sb.append("<parameter><value>").append(p)
242 .append("</value></parameter>");
246 return sb.toString();
249 public Process launchPymol()
251 // todo pull up much of this
252 // Do nothing if already launched
253 if (isPymolLaunched())
258 String error = "Error message: ";
259 for (String pymolPath : getPymolPaths())
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())
270 error += "File '" + path + "' does not exist.\n";
273 List<String> args = new ArrayList<>();
276 // Windows PyMOLWin.exe needs an extra argument
277 if (Platform.isWin() && pymolPath.toLowerCase(Locale.ROOT)
278 .endsWith("\\pymolwin.exe"))
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();
288 } catch (Exception e)
290 // Pymol could not be started using this path
291 error += e.getMessage();
295 if (pymolProcess != null)
297 this.pymolXmlRpcPort = getPortNumber();
298 if (pymolXmlRpcPort > 0)
300 Console.info("PyMOL XMLRPC started on port " + pymolXmlRpcPort);
304 error += "Failed to read PyMOL XMLRPC port number";
305 Console.error(error);
306 pymolProcess.destroy();
314 private int getPortNumber()
316 // TODO pull up most of this!
318 InputStream readChan = pymolProcess.getInputStream();
319 BufferedReader lineReader = new BufferedReader(
320 new InputStreamReader(readChan));
321 StringBuilder responses = new StringBuilder();
324 String response = lineReader.readLine();
325 while (response != null)
327 responses.append("\n" + response);
328 // expect: xml-rpc server running on host localhost, port 9123
329 if (response.contains("xml-rpc"))
331 String[] tokens = response.split(" ");
332 for (int i = 0; i < tokens.length - 1; i++)
334 if ("port".equals(tokens[i]))
336 port = Integer.parseInt(tokens[i + 1]);
343 break; // hack for hanging readLine()
345 response = lineReader.readLine();
347 } catch (Exception e)
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());
358 } catch (IOException e2)
364 Console.error("Failed to start PyMOL with XMLRPC, response was: "
367 Console.info("PyMOL started with XMLRPC on port " + port);