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