JAL-1677 check all response lines when parsing REST server port
[jalview.git] / src / ext / edu / ucsf / rbvi / strucviz2 / ChimeraManager.java
1 package ext.edu.ucsf.rbvi.strucviz2;
2
3 import jalview.ws.HttpClientUtils;
4
5 import java.awt.Color;
6 import java.io.BufferedReader;
7 import java.io.File;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.io.InputStreamReader;
11 import java.util.ArrayList;
12 import java.util.Collection;
13 import java.util.HashMap;
14 import java.util.List;
15 import java.util.Map;
16
17 import org.apache.http.NameValuePair;
18 import org.apache.http.message.BasicNameValuePair;
19 import org.slf4j.Logger;
20 import org.slf4j.LoggerFactory;
21
22 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
23 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
24
25 /**
26  * This object maintains the Chimera communication information.
27  */
28 public class ChimeraManager
29 {
30   private static final boolean debug = false;
31
32   private int chimeraRestPort;
33
34   private Process chimera;
35
36   private ListenerThreads chimeraListenerThread;
37
38   private Map<Integer, ChimeraModel> currentModelsMap;
39
40   private Logger logger = LoggerFactory
41           .getLogger(ext.edu.ucsf.rbvi.strucviz2.ChimeraManager.class);
42
43   private StructureManager structureManager;
44
45   public ChimeraManager(StructureManager structureManager)
46   {
47     this.structureManager = structureManager;
48     chimera = null;
49     chimeraListenerThread = null;
50     currentModelsMap = new HashMap<Integer, ChimeraModel>();
51
52   }
53
54   public List<ChimeraModel> getChimeraModels(String modelName)
55   {
56     List<ChimeraModel> models = getChimeraModels(modelName,
57             ModelType.PDB_MODEL);
58     models.addAll(getChimeraModels(modelName, ModelType.SMILES));
59     return models;
60   }
61
62   public List<ChimeraModel> getChimeraModels(String modelName,
63           ModelType modelType)
64   {
65     List<ChimeraModel> models = new ArrayList<ChimeraModel>();
66     for (ChimeraModel model : currentModelsMap.values())
67     {
68       if (modelName.equals(model.getModelName())
69               && modelType.equals(model.getModelType()))
70       {
71         models.add(model);
72       }
73     }
74     return models;
75   }
76
77   public Map<String, List<ChimeraModel>> getChimeraModelsMap()
78   {
79     Map<String, List<ChimeraModel>> models = new HashMap<String, List<ChimeraModel>>();
80     for (ChimeraModel model : currentModelsMap.values())
81     {
82       String modelName = model.getModelName();
83       if (!models.containsKey(modelName))
84       {
85         models.put(modelName, new ArrayList<ChimeraModel>());
86       }
87       if (!models.get(modelName).contains(model))
88       {
89         models.get(modelName).add(model);
90       }
91     }
92     return models;
93   }
94
95   public ChimeraModel getChimeraModel(Integer modelNumber,
96           Integer subModelNumber)
97   {
98     Integer key = ChimUtils.makeModelKey(modelNumber, subModelNumber);
99     if (currentModelsMap.containsKey(key))
100     {
101       return currentModelsMap.get(key);
102     }
103     return null;
104   }
105
106   public ChimeraModel getChimeraModel()
107   {
108     return currentModelsMap.values().iterator().next();
109   }
110
111   public Collection<ChimeraModel> getChimeraModels()
112   {
113     // this method is invoked by the model navigator dialog
114     return currentModelsMap.values();
115   }
116
117   public int getChimeraModelsCount(boolean smiles)
118   {
119     // this method is invokes by the model navigator dialog
120     int counter = currentModelsMap.size();
121     if (smiles)
122     {
123       return counter;
124     }
125
126     for (ChimeraModel model : currentModelsMap.values())
127     {
128       if (model.getModelType() == ModelType.SMILES)
129       {
130         counter--;
131       }
132     }
133     return counter;
134   }
135
136   public boolean hasChimeraModel(Integer modelNubmer)
137   {
138     return hasChimeraModel(modelNubmer, 0);
139   }
140
141   public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber)
142   {
143     return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer,
144             subModelNumber));
145   }
146
147   public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
148           ChimeraModel model)
149   {
150     currentModelsMap.put(
151             ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
152   }
153
154   public void removeChimeraModel(Integer modelNumber, Integer subModelNumber)
155   {
156     int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
157     if (currentModelsMap.containsKey(modelKey))
158     {
159       currentModelsMap.remove(modelKey);
160     }
161   }
162
163   public List<ChimeraModel> openModel(String modelPath, ModelType type)
164   {
165     return openModel(modelPath, getFileNameFromPath(modelPath), type);
166   }
167
168   /**
169    * Overloaded method to allow Jalview to pass in a model name.
170    * 
171    * @param modelPath
172    * @param modelName
173    * @param type
174    * @return
175    */
176   public List<ChimeraModel> openModel(String modelPath, String modelName,
177           ModelType type)
178   {
179     logger.info("chimera open " + modelPath);
180     // stopListening();
181     List<String> response = null;
182     // TODO: [Optional] Handle modbase models
183     if (type == ModelType.MODBASE_MODEL)
184     {
185       response = sendChimeraCommand("open modbase:" + modelPath, true);
186       // } else if (type == ModelType.SMILES) {
187       // response = sendChimeraCommand("open smiles:" + modelName, true);
188       // modelName = "smiles:" + modelName;
189     }
190     else
191     {
192       response = sendChimeraCommand("open " + modelPath, true);
193     }
194     if (response == null)
195     {
196       // something went wrong
197       logger.warn("Could not open " + modelPath);
198       return null;
199     }
200     List<ChimeraModel> models = new ArrayList<ChimeraModel>();
201     int[] modelNumbers = null;
202     if (type == ModelType.PDB_MODEL)
203     {
204       for (String line : response)
205       {
206         if (line.startsWith("#"))
207         {
208           modelNumbers = ChimUtils.parseOpenedModelNumber(line);
209           if (modelNumbers != null)
210           {
211             int modelNumber = ChimUtils.makeModelKey(modelNumbers[0],
212                     modelNumbers[1]);
213             if (currentModelsMap.containsKey(modelNumber))
214             {
215               continue;
216             }
217             ChimeraModel newModel = new ChimeraModel(modelName, type,
218                     modelNumbers[0], modelNumbers[1]);
219             currentModelsMap.put(modelNumber, newModel);
220             models.add(newModel);
221
222             //
223             // patch for Jalview - set model name in Chimera
224             // TODO: find a variant that works for sub-models
225             sendChimeraCommand("setattr M name " + modelName + " #"
226                     + modelNumbers[0], false);
227             // end patch for Jalview
228
229             modelNumbers = null;
230           }
231         }
232       }
233     }
234     else
235     {
236       // TODO: [Optional] Open smiles from file would fail. Do we need it?
237       // If parsing fails, iterate over all open models to get the right one
238       List<ChimeraModel> openModels = getModelList();
239       for (ChimeraModel openModel : openModels)
240       {
241         String openModelName = openModel.getModelName();
242         if (openModelName.endsWith("..."))
243         {
244           openModelName = openModelName.substring(0,
245                   openModelName.length() - 3);
246         }
247         if (modelPath.startsWith(openModelName))
248         {
249           openModel.setModelName(modelPath);
250           int modelNumber = ChimUtils
251                   .makeModelKey(openModel.getModelNumber(),
252                           openModel.getSubModelNumber());
253           if (!currentModelsMap.containsKey(modelNumber))
254           {
255             currentModelsMap.put(modelNumber, openModel);
256             models.add(openModel);
257           }
258         }
259       }
260     }
261
262     // assign color and residues to open models
263     for (ChimeraModel newModel : models)
264     {
265       // get model color
266       Color modelColor = getModelColor(newModel);
267       if (modelColor != null)
268       {
269         newModel.setModelColor(modelColor);
270       }
271
272       // Get our properties (default color scheme, etc.)
273       // Make the molecule look decent
274       // chimeraSend("repr stick "+newModel.toSpec());
275
276       // Create the information we need for the navigator
277       if (type != ModelType.SMILES)
278       {
279         addResidues(newModel);
280       }
281     }
282
283     sendChimeraCommand("focus", false);
284     // startListening(); // see ChimeraListener
285     return models;
286   }
287
288   /**
289    * Refactored method to extract the last (or only) element delimited by file
290    * path separator.
291    * 
292    * @param modelPath
293    * @return
294    */
295   private String getFileNameFromPath(String modelPath)
296   {
297     String modelName = modelPath;
298     if (modelPath == null)
299     {
300       return null;
301     }
302     // TODO: [Optional] Convert path to name in a better way
303     if (modelPath.lastIndexOf(File.separator) > 0)
304     {
305       modelName = modelPath.substring(modelPath
306               .lastIndexOf(File.separator) + 1);
307     }
308     else if (modelPath.lastIndexOf("/") > 0)
309     {
310       modelName = modelPath
311               .substring(modelPath.lastIndexOf("/") + 1);
312     }
313     return modelName;
314   }
315
316   public void closeModel(ChimeraModel model)
317   {
318     // int model = structure.modelNumber();
319     // int subModel = structure.subModelNumber();
320     // Integer modelKey = makeModelKey(model, subModel);
321     stopListening();
322     logger.info("chimera close model " + model.getModelName());
323     if (currentModelsMap.containsKey(ChimUtils.makeModelKey(
324             model.getModelNumber(), model.getSubModelNumber())))
325     {
326       sendChimeraCommand("close " + model.toSpec(), false);
327       // currentModelNamesMap.remove(model.getModelName());
328       currentModelsMap.remove(ChimUtils.makeModelKey(
329               model.getModelNumber(), model.getSubModelNumber()));
330       // selectionList.remove(chimeraModel);
331     }
332     else
333     {
334       logger.warn("Could not find model " + model.getModelName()
335               + " to close.");
336     }
337     startListening();
338   }
339
340   public void startListening()
341   {
342     sendChimeraCommand("listen start models; listen start selection", false);
343   }
344
345   public void stopListening()
346   {
347     sendChimeraCommand("listen stop models ; listen stop selection ", false);
348   }
349
350   /**
351    * Tell Chimera we are listening on the given URI
352    * 
353    * @param uri
354    */
355   public void startListening(String uri)
356   {
357     sendChimeraCommand("listen start models url " + uri
358     + ";listen start select prefix SelectionChanged url " + uri, false);
359   }
360
361   /**
362    * Select something in Chimera
363    * 
364    * @param command
365    *          the selection command to pass to Chimera
366    */
367   public void select(String command)
368   {
369     sendChimeraCommand("listen stop selection; " + command
370             + "; listen start selection", false);
371   }
372
373   public void focus()
374   {
375     sendChimeraCommand("focus", false);
376   }
377
378   public void clearOnChimeraExit()
379   {
380     chimera = null;
381     currentModelsMap.clear();
382       this.chimeraRestPort = 0;
383     structureManager.clearOnChimeraExit();
384   }
385
386   public void exitChimera()
387   {
388     if (isChimeraLaunched() && chimera != null)
389     {
390       sendChimeraCommand("stop really", false);
391       try
392       {
393         chimera.destroy();
394       } catch (Exception ex)
395       {
396         // ignore
397       }
398     }
399     clearOnChimeraExit();
400   }
401
402   public Map<Integer, ChimeraModel> getSelectedModels()
403   {
404     Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
405     List<String> chimeraReply = sendChimeraCommand(
406             "list selection level molecule", true);
407     if (chimeraReply != null)
408     {
409       for (String modelLine : chimeraReply)
410       {
411         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
412         Integer modelKey = ChimUtils.makeModelKey(
413                 chimeraModel.getModelNumber(),
414                 chimeraModel.getSubModelNumber());
415         selectedModelsMap.put(modelKey, chimeraModel);
416       }
417     }
418     return selectedModelsMap;
419   }
420
421   /**
422    * Sends a 'list selection level residue' command to Chimera and returns the
423    * list of selected atomspecs
424    * 
425    * @return
426    */
427   public List<String> getSelectedResidueSpecs()
428   {
429     List<String> selectedResidues = new ArrayList<String>();
430     List<String> chimeraReply = sendChimeraCommand(
431             "list selection level residue", true);
432     if (chimeraReply != null)
433     {
434       for (String inputLine : chimeraReply)
435       {
436         String[] inputLineParts = inputLine.split("\\s+");
437         if (inputLineParts.length == 5)
438         {
439           selectedResidues.add(inputLineParts[2]);
440         }
441       }
442     }
443     return selectedResidues;
444   }
445
446   public void getSelectedResidues(
447           Map<Integer, ChimeraModel> selectedModelsMap)
448   {
449     List<String> chimeraReply = sendChimeraCommand(
450             "list selection level residue", true);
451     if (chimeraReply != null)
452     {
453       for (String inputLine : chimeraReply)
454       {
455         ChimeraResidue r = new ChimeraResidue(inputLine);
456         Integer modelKey = ChimUtils.makeModelKey(r.getModelNumber(),
457                 r.getSubModelNumber());
458         if (selectedModelsMap.containsKey(modelKey))
459         {
460           ChimeraModel model = selectedModelsMap.get(modelKey);
461           model.addResidue(r);
462         }
463       }
464     }
465   }
466
467   /**
468    * Return the list of ChimeraModels currently open. Warning: if smiles model
469    * name too long, only part of it with "..." is printed.
470    * 
471    * 
472    * @return List of ChimeraModel's
473    */
474   // TODO: [Optional] Handle smiles names in a better way in Chimera?
475   public List<ChimeraModel> getModelList()
476   {
477     List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
478     List<String> list = sendChimeraCommand("list models type molecule",
479             true);
480     if (list != null)
481     {
482       for (String modelLine : list)
483       {
484         ChimeraModel chimeraModel = new ChimeraModel(modelLine);
485         modelList.add(chimeraModel);
486       }
487     }
488     return modelList;
489   }
490
491   /**
492    * Return the list of depiction presets available from within Chimera. Chimera
493    * will return the list as a series of lines with the format: Preset type
494    * number "description"
495    * 
496    * @return list of presets
497    */
498   public List<String> getPresets()
499   {
500     ArrayList<String> presetList = new ArrayList<String>();
501     List<String> output = sendChimeraCommand("preset list", true);
502     if (output != null)
503     {
504       for (String preset : output)
505       {
506         preset = preset.substring(7); // Skip over the "Preset"
507         preset = preset.replaceFirst("\"", "(");
508         preset = preset.replaceFirst("\"", ")");
509         // string now looks like: type number (description)
510         presetList.add(preset);
511       }
512     }
513     return presetList;
514   }
515
516   public boolean isChimeraLaunched()
517   {
518     boolean launched = false;
519     if (chimera != null)
520     {
521       try
522       {
523         chimera.exitValue();
524         // if we get here, process has ended
525       } catch (IllegalThreadStateException e)
526       {
527         // ok - not yet terminated
528         launched = true;
529       }
530     }
531     return launched;
532   }
533
534   /**
535    * Launch Chimera, unless an instance linked to this object is already
536    * running. Returns true if chimera is successfully launched, or already
537    * running, else false.
538    * 
539    * @param chimeraPaths
540    * @return
541    */
542   public boolean launchChimera(List<String> chimeraPaths)
543   {
544     // Do nothing if Chimera is already launched
545     if (isChimeraLaunched())
546     {
547       return true;
548     }
549
550     // Try to launch Chimera (eventually using one of the possible paths)
551     String error = "Error message: ";
552     String workingPath = "";
553     // iterate over possible paths for starting Chimera
554     for (String chimeraPath : chimeraPaths)
555     {
556       File path = new File(chimeraPath);
557       // uncomment the next line to simulate Chimera not installed
558       // path = new File(chimeraPath + "x");
559       if (!path.canExecute())
560       {
561         error += "File '" + path + "' does not exist.\n";
562         continue;
563       }
564       try
565       {
566         List<String> args = new ArrayList<String>();
567         args.add(chimeraPath);
568         // shows Chimera output window but suppresses REST responses:
569         // args.add("--debug");
570         args.add("--start");
571         args.add("RESTServer");
572         ProcessBuilder pb = new ProcessBuilder(args);
573         chimera = pb.start();
574         error = "";
575         workingPath = chimeraPath;
576         break;
577       } catch (Exception e)
578       {
579         // Chimera could not be started
580         error += e.getMessage();
581       }
582     }
583     // If no error, then Chimera was launched successfully
584     if (error.length() == 0)
585     {
586       this.chimeraRestPort = getPortNumber();
587       System.out.println("Chimera REST API started on port "
588               + chimeraRestPort);
589       // structureManager.initChimTable();
590       structureManager.setChimeraPathProperty(workingPath);
591       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
592       // Ask Chimera to give us updates
593       // startListening(); // later - see ChimeraListener
594       return (chimeraRestPort > 0);
595     }
596
597     // Tell the user that Chimera could not be started because of an error
598     logger.warn(error);
599     return false;
600   }
601
602   /**
603    * Read and return the port number returned in the reply to --start RESTServer
604    */
605   private int getPortNumber()
606   {
607     int port = 0;
608     InputStream readChan = chimera.getInputStream();
609     BufferedReader lineReader = new BufferedReader(new InputStreamReader(
610             readChan));
611     StringBuilder responses = new StringBuilder();
612     try
613     {
614       String response = lineReader.readLine();
615       while (response != null)
616       {
617         responses.append("\n" + response);
618         // expect: REST server on host 127.0.0.1 port port_number
619         if (response.startsWith("REST server"))
620         {
621           String[] tokens = response.split(" ");
622           if (tokens.length == 7 && "port".equals(tokens[5]))
623           {
624             port = Integer.parseInt(tokens[6]);
625             break;
626           }
627         }
628         response = lineReader.readLine();
629       }
630     } catch (Exception e)
631     {
632       logger.error("Failed to get REST port number from " + responses
633               + ": "
634               + e.getMessage());
635     } finally
636     {
637       try
638       {
639         lineReader.close();
640       } catch (IOException e2)
641       {
642       }
643     }
644     if (port == 0)
645     {
646       System.err
647               .println("Failed to start Chimera with REST service, response was: "
648                       + responses);
649     }
650     logger.info("Chimera REST service listening on port " + chimeraRestPort);
651     return port;
652   }
653
654   /**
655    * Determine the color that Chimera is using for this model.
656    * 
657    * @param model
658    *          the ChimeraModel we want to get the Color for
659    * @return the default model Color for this model in Chimera
660    */
661   public Color getModelColor(ChimeraModel model)
662   {
663     List<String> colorLines = sendChimeraCommand(
664             "list model spec " + model.toSpec() + " attribute color", true);
665     if (colorLines == null || colorLines.size() == 0)
666     {
667       return null;
668     }
669     return ChimUtils.parseModelColor(colorLines.get(0));
670   }
671
672   /**
673    * 
674    * Get information about the residues associated with a model. This uses the
675    * Chimera listr command. We don't return the resulting residues, but we add
676    * the residues to the model.
677    * 
678    * @param model
679    *          the ChimeraModel to get residue information for
680    * 
681    */
682   public void addResidues(ChimeraModel model)
683   {
684     int modelNumber = model.getModelNumber();
685     int subModelNumber = model.getSubModelNumber();
686     // Get the list -- it will be in the reply log
687     List<String> reply = sendChimeraCommand(
688             "list residues spec " + model.toSpec(), true);
689     if (reply == null)
690     {
691       return;
692     }
693     for (String inputLine : reply)
694     {
695       ChimeraResidue r = new ChimeraResidue(inputLine);
696       if (r.getModelNumber() == modelNumber
697               || r.getSubModelNumber() == subModelNumber)
698       {
699         model.addResidue(r);
700       }
701     }
702   }
703
704   public List<String> getAttrList()
705   {
706     List<String> attributes = new ArrayList<String>();
707     final List<String> reply = sendChimeraCommand("list resattr", true);
708     if (reply != null)
709     {
710       for (String inputLine : reply)
711       {
712         String[] lineParts = inputLine.split("\\s");
713         if (lineParts.length == 2 && lineParts[0].equals("resattr"))
714         {
715           attributes.add(lineParts[1]);
716         }
717       }
718     }
719     return attributes;
720   }
721
722   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
723           ChimeraModel model)
724   {
725     Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
726     final List<String> reply = sendChimeraCommand("list residue spec "
727             + model.toSpec() + " attribute " + aCommand, true);
728     if (reply != null)
729     {
730       for (String inputLine : reply)
731       {
732         String[] lineParts = inputLine.split("\\s");
733         if (lineParts.length == 5)
734         {
735           ChimeraResidue residue = ChimUtils
736                   .getResidue(lineParts[2], model);
737           String value = lineParts[4];
738           if (residue != null)
739           {
740             if (value.equals("None"))
741             {
742               continue;
743             }
744             if (value.equals("True") || value.equals("False"))
745             {
746               values.put(residue, Boolean.valueOf(value));
747               continue;
748             }
749             try
750             {
751               Double doubleValue = Double.valueOf(value);
752               values.put(residue, doubleValue);
753             } catch (NumberFormatException ex)
754             {
755               values.put(residue, value);
756             }
757           }
758         }
759       }
760     }
761     return values;
762   }
763
764   private volatile boolean busy = false;
765
766   /**
767    * Send a command to Chimera.
768    * 
769    * @param command
770    *          Command string to be send.
771    * @param reply
772    *          Flag indicating whether the method should return the reply from
773    *          Chimera or not.
774    * @return List of Strings corresponding to the lines in the Chimera reply or
775    *         <code>null</code>.
776    */
777   public List<String> sendChimeraCommand(String command, boolean reply)
778   {
779 //    System.out.println("chimeradebug>> " + command);
780     if (!isChimeraLaunched() || command == null
781             || "".equals(command.trim()))
782     {
783       return null;
784     }
785     // TODO do we need a maximum wait time before aborting?
786     while (busy)
787     {
788       try
789       {
790         Thread.sleep(25);
791       } catch (InterruptedException q)
792       {
793       }
794     }
795     busy = true;
796     long startTime = System.currentTimeMillis();
797     try
798     {
799       return sendRestCommand(command);
800     } finally
801     {
802       /*
803        * Make sure busy flag is reset come what may!
804        */
805       busy = false;
806       if (debug)
807       {
808         System.out.println("Chimera command took "
809                 + (System.currentTimeMillis() - startTime) + "ms: "
810                 + command);
811       }
812
813     }
814   }
815
816   /**
817    * Sends the command to Chimera's REST API, and returns any response lines.
818    * 
819    * @param command
820    * @return
821    */
822   protected List<String> sendRestCommand(String command)
823   {
824     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
825     List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
826     commands.add(new BasicNameValuePair("command", command));
827
828     List<String> reply = new ArrayList<String>();
829     BufferedReader response = null;
830     try {
831       response = HttpClientUtils
832               .doHttpUrlPost(restUrl, commands, 100, 5000);
833       String line = "";
834       while ((line = response.readLine()) != null) {
835         reply.add(line);
836       }
837     } catch (Exception e)
838     {
839       logger.error("REST call " + command + " failed: " + e.getMessage());
840     } finally
841     {
842       if (response != null)
843       {
844         try
845         {
846           response.close();
847         } catch (IOException e)
848         {
849         }
850       }
851     }
852     return reply;
853   }
854
855   /**
856    * Send a command to stdin of Chimera process, and optionally read any
857    * responses.
858    * 
859    * @param command
860    * @param readReply
861    * @return
862    */
863   protected List<String> sendStdinCommand(String command, boolean readReply)
864   {
865     chimeraListenerThread.clearResponse(command);
866     String text = command.concat("\n");
867     try
868     {
869       // send the command
870       chimera.getOutputStream().write(text.getBytes());
871       chimera.getOutputStream().flush();
872     } catch (IOException e)
873     {
874       // logger.info("Unable to execute command: " + text);
875       // logger.info("Exiting...");
876       logger.warn("Unable to execute command: " + text);
877       logger.warn("Exiting...");
878       clearOnChimeraExit();
879       return null;
880     }
881     if (!readReply)
882     {
883       return null;
884     }
885     List<String> rsp = chimeraListenerThread.getResponse(command);
886     return rsp;
887   }
888
889   public StructureManager getStructureManager()
890   {
891     return structureManager;
892   }
893
894   public boolean isBusy()
895   {
896     return busy;
897   }
898 }