View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2024.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx;
39  
40  import edu.rit.pj.Comm;
41  import edu.rit.pj.cluster.Configuration;
42  import ffx.potential.Utilities;
43  import ffx.ui.LogHandler;
44  import ffx.ui.MainPanel;
45  import ffx.ui.ModelingShell;
46  import ffx.ui.OSXAdapter;
47  import ffx.utilities.FFXScript;
48  import groovy.lang.Script;
49  import org.apache.commons.io.FilenameUtils;
50  import org.apache.commons.lang3.SystemUtils;
51  import org.apache.commons.lang3.builder.ToStringBuilder;
52  import org.apache.commons.lang3.time.StopWatch;
53  import org.apache.log4j.PropertyConfigurator;
54  
55  import javax.annotation.Nullable;
56  import javax.swing.ImageIcon;
57  import javax.swing.JFrame;
58  import javax.swing.SwingUtilities;
59  import javax.swing.UIManager;
60  import java.awt.GraphicsEnvironment;
61  import java.awt.Toolkit;
62  import java.awt.event.WindowAdapter;
63  import java.awt.event.WindowEvent;
64  import java.io.File;
65  import java.io.IOException;
66  import java.net.InetAddress;
67  import java.net.URL;
68  import java.net.UnknownHostException;
69  import java.util.ArrayList;
70  import java.util.Arrays;
71  import java.util.Date;
72  import java.util.List;
73  import java.util.Objects;
74  import java.util.Properties;
75  import java.util.logging.Handler;
76  import java.util.logging.Level;
77  import java.util.logging.LogManager;
78  import java.util.logging.Logger;
79  
80  import static java.lang.String.format;
81  
82  /**
83   * The Main class is the entry point to the graphical user interface version of Force Field X.
84   *
85   * @author Michael J. Schnieders
86   * @since 1.0
87   */
88  public final class Main extends JFrame {
89  
90    private static final Logger logger = Logger.getLogger(Main.class.getName());
91    /** This is the main application wrapper. */
92    public static MainPanel mainPanel;
93    /** Constant <code>stopWatch</code> */
94    private static StopWatch stopWatch = new StopWatch();
95    /** Handle FFX logging. */
96    private static LogHandler logHandler;
97    /** The configured Scheduler port. */
98    private static int schedulerPort;
99    /** Parallel Java Configuration instance. */
100   private static Configuration configuration = null;
101   /** Parallel Java World Communicator. */
102   private static Comm world = null;
103   /** Name of the machine FFX is running on. */
104   private static String hostName = null;
105   /** Force Field X process ID. */
106   private static int procID = -1;
107   /** Print version and exit */
108   private static boolean printVersionAndExit = false;
109   /** This Runnable is used to init the GUI using SwingUtilities. */
110   private final Runnable initGUI =
111       new Runnable() {
112         // Create the MainPanel and MainMenu, then add them to the JFrame.
113         public void run() {
114 
115           Toolkit.getDefaultToolkit().setDynamicLayout(true);
116 
117           // Set the Title and Icon
118           Main.this.setTitle("Force Field X");
119           URL iconURL = getClass().getClassLoader().getResource("ffx/ui/icons/icon64.png");
120           if (iconURL != null) {
121             ImageIcon icon = new ImageIcon(iconURL);
122             Main.this.setIconImage(icon.getImage());
123           }
124           Main.this.addWindowListener(
125               new WindowAdapter() {
126                 @Override
127                 public void windowClosing(WindowEvent e) {
128                   if (mainPanel != null) {
129                     mainPanel.exit();
130                   }
131                   System.exit(0);
132                 }
133               });
134 
135           mainPanel = new MainPanel(Main.this);
136           mainPanel.setVisible(false);
137           logHandler.setMainPanel(mainPanel);
138           Main.this.add(mainPanel);
139           mainPanel.initialize();
140           Main.this.setVisible(true);
141           mainPanel.setVisible(true);
142           Main.this.setJMenuBar(mainPanel.getMainMenu());
143 
144           // This is a hack to get GraphicsCanvis to initialize on some platform/Java3D
145           // combinations.
146           // mainPanel.setPanel(MainPanel.KEYWORDS);
147           // mainPanel.setVisible(true);
148           // mainPanel.setPanel(MainPanel.GRAPHICS);
149 
150           // Mac OS X specific features that help Force Field X look native
151           // on Macs. This needs to be done after the MainPanel is created.
152           if (SystemUtils.IS_OS_MAC_OSX) {
153             OSXAdapter osxAdapter = new OSXAdapter(mainPanel);
154           }
155 
156           SwingUtilities.updateComponentTreeUI(SwingUtilities.getRoot(Main.this));
157         }
158       };
159 
160   /**
161    * Main does some window initializations.
162    *
163    * @param commandLineFile a {@link java.io.File} object.
164    * @param argList a {@link java.util.List} object.
165    */
166   public Main(@Nullable File commandLineFile, List<String> argList) {
167     super("Force Field X");
168     // Start the clock.
169     stopWatch.start();
170 
171     // Init the GUI.
172     try {
173       SwingUtilities.invokeAndWait(initGUI);
174     } catch (Exception e) {
175       logger.warning(" Exception initializing the GUI.\n" + e);
176     }
177 
178     // Run the supplied command or file.
179     if (commandLineFile != null) {
180       runScript(mainPanel.getModelingShell(), commandLineFile, argList);
181     }
182 
183     if (logger.isLoggable(Level.FINE)) {
184       StringBuilder sb = new StringBuilder();
185       sb.append(format("\n Start-up Time (msec): %s.", stopWatch.getTime()));
186       Runtime runtime = Runtime.getRuntime();
187       runtime.gc();
188       long occupiedMemory = runtime.totalMemory() - runtime.freeMemory();
189       long KB = 1024;
190       sb.append(format("\n In-Use Memory   (Kb): %d", occupiedMemory / KB));
191       sb.append(format("\n Free Memory     (Kb): %d", runtime.freeMemory() / KB));
192       sb.append(format("\n Total Memory    (Kb): %d", runtime.totalMemory() / KB));
193       logger.fine(sb.toString());
194     }
195   }
196 
197   /**
198    * Create an instance of Force Field X
199    *
200    * @param args an array of {@link java.lang.String} objects.
201    */
202   public static void main(String[] args) {
203 
204     List<String> argList = new ArrayList<>();
205     File commandLineFile = initMain(args, argList);
206 
207     try {
208       // Start up the GUI or CLI version of Force Field X.
209       if (!GraphicsEnvironment.isHeadless()) {
210         startGraphicalUserInterface(commandLineFile, argList);
211       } else {
212         if (commandLineFile == null) {
213           logger.info(" No command line file supplied.");
214         } else {
215           startCommandLineInterface(commandLineFile, argList);
216         }
217       }
218     } catch (Throwable t) {
219       int statusCode = 1;
220       logger.info(" Uncaught exception: exiting with status code " + statusCode);
221       logger.info(Utilities.stackTraceToString(t));
222       System.exit(statusCode);
223     }
224   }
225 
226   /**
227    * Process the input arguments into a List, start the logging, start Parallel Java and process the
228    * input command.
229    *
230    * @param args an array of {@link java.lang.String} objects.
231    * @param argList List is filled with processed arguments.
232    * @return A file for FFX to operate on.
233    */
234   public static File initMain(String[] args, List<String> argList) {
235     // Process any "-D" command line flags.
236     args = processProperties(args);
237 
238     // Start up the Parallel Java communication layer.
239     startParallelJava(args);
240 
241     // Configure our logging.
242     startLogging();
243 
244     // Determine host name and process ID.
245     environment();
246 
247     // Print the header.
248     header(args);
249 
250     // Print out help for the command line interface.
251     if (GraphicsEnvironment.isHeadless() && args.length < 2) {
252       if (args.length == 1 && args[0].toUpperCase().contains("TEST")) {
253         commandLineInterfaceHelp(true);
254       }
255       commandLineInterfaceHelp(false);
256     }
257 
258     // Parse the specified command or structure file.
259     File commandLineFile = null;
260     int nArgs = args.length;
261     if (nArgs > 0) {
262       commandLineFile = new File(args[0]);
263       // Resolve a relavtive path
264       if (commandLineFile.exists()) {
265         commandLineFile = new File(FilenameUtils.normalize(commandLineFile.getAbsolutePath()));
266       }
267     }
268 
269     // Convert the args to a List<String>.
270     if (nArgs > 1) {
271       argList.addAll(Arrays.asList(args).subList(1, nArgs));
272     }
273 
274     return commandLineFile;
275   }
276 
277   /**
278    * A main entry point that runs a script and return a refernce to the result.
279    *
280    * @param args an array of {@link java.lang.String} objects.
281    * @return A Groovy Script instance.
282    */
283   public static Script ffxScript(String[] args) {
284     List<String> argList = new ArrayList<>();
285     File commandLineFile = initMain(args, argList);
286     try {
287       if (commandLineFile == null) {
288         logger.info(" No command line file supplied.");
289       } else {
290         return startCommandLineInterface(commandLineFile, argList);
291       }
292     } catch (Throwable t) {
293       int statusCode = 1;
294       logger.info(" Uncaught exception: exiting with status code " + statusCode);
295       logger.info(Utilities.stackTraceToString(t));
296       System.exit(statusCode);
297     }
298     return null;
299   }
300 
301   /** Print out help for the command line version of Force Field X. */
302   private static void commandLineInterfaceHelp(boolean listTestScripts) {
303     logger.info(" usage: ffxc [-D<property=value>] <command> [-options] <PDB|XYZ>");
304     if (listTestScripts) {
305       FFXScript.listGroovyScripts(false, true);
306       logger.info("\n For help on an experimental or test command use:  ffxc <command> -h\n");
307     } else {
308       FFXScript.listGroovyScripts(true, false);
309       logger.info("\n To list experimental & test scripts: ffxc --test");
310       logger.info(" For help on a specific command use:  ffxc <command> -h\n");
311     }
312     System.exit(0);
313   }
314 
315   /** Determine the host name, process ID, and FFX base directory. */
316   private static void environment() {
317     try {
318       InetAddress addr = InetAddress.getLocalHost();
319       hostName = addr.getHostName();
320     } catch (UnknownHostException e) {
321       // Do nothing.
322     }
323 
324     String procString = System.getProperty("app.pid");
325     if (procString != null) {
326       procID = Integer.parseInt(procString);
327     } else {
328       procID = 0;
329     }
330 
331     String dirString = System.getProperty("basedir");
332     File ffxDirectory;
333     if (dirString != null) {
334       ffxDirectory = new File(dirString);
335     } else {
336       ffxDirectory = new File(".");
337     }
338 
339     try {
340       logger.fine(format(" Force Field X directory is %s", ffxDirectory.getCanonicalPath()));
341     } catch (Exception e) {
342       // Do Nothing.
343     }
344   }
345 
346   /** Print out a promo. */
347   private static void header(String[] args) {
348     StringBuilder sb = new StringBuilder();
349     sb.append(MainPanel.border).append("\n");
350     sb.append(MainPanel.title).append("\n");
351     sb.append(MainPanel.aboutString).append("\n");
352     sb.append(MainPanel.border);
353 
354     // Print the FFX version and exit.
355     if (printVersionAndExit && GraphicsEnvironment.isHeadless()) {
356       logger.info(sb.toString());
357       System.exit(0);
358     }
359 
360     sb.append("\n ").append(new Date());
361     sb.append(format("\n Process ID %d on %s.", procID, hostName));
362 
363     // Print out command line arguments if the array is not null.
364     if (args != null && args.length > 0) {
365       sb.append("\n\n Command line arguments:\n ");
366       sb.append(Arrays.toString(args));
367       sb.append("\n");
368     }
369 
370     if (schedulerPort > 1) {
371       sb.append(format("\n Parallel Java:\n %s", world.toString()));
372       if (configuration != null) {
373         sb.append(
374             format(
375                 "\n Scheduler %s on port %d.\n", configuration.getSchedulerHost(), schedulerPort));
376       }
377     }
378 
379     logger.info(sb.toString());
380   }
381 
382   /** Process any "-D" command line flags. */
383   private static String[] processProperties(String[] args) {
384     List<String> newArgs = new ArrayList<>();
385     for (String arg : args) {
386       arg = arg.trim();
387 
388       if (arg.equals("-V") || arg.equals("--version")) {
389         printVersionAndExit = true;
390       }
391 
392       if (arg.startsWith("-D")) {
393         // Remove -D from the front of String.
394         arg = arg.substring(2);
395         // Split at the first equals if it exists.
396         if (arg.contains("=")) {
397           int equalsPosition = arg.indexOf("=");
398           String key = arg.substring(0, equalsPosition);
399           String value = arg.substring(equalsPosition + 1);
400           // Set the system property.
401           System.setProperty(key, value);
402         } else {
403           if (!arg.isEmpty()) {
404             System.setProperty(arg, "");
405           }
406         }
407       } else {
408         // Collect non "-D" arguments.
409         newArgs.add(arg);
410       }
411     }
412     // Return the remaining arguments.
413     args = new String[newArgs.size()];
414     newArgs.toArray(args);
415     return args;
416   }
417 
418   /**
419    * Start the Force Field X command line interface.
420    *
421    * @param commandLineFile The command line file.
422    * @param argList The command line argument list.
423    */
424   private static Script startCommandLineInterface(File commandLineFile, List<String> argList) {
425     if (configuration == null) {
426       logger.info(" Starting up the command line interface.\n");
427     }
428     HeadlessMain m = new HeadlessMain(logHandler);
429     mainPanel = m.mainPanel;
430     return runScript(mainPanel.getModelingShell(), commandLineFile, argList);
431   }
432 
433   /**
434    * Start the Force Field X graphical user interface.
435    *
436    * @param commandLineFile The command line file.
437    * @param argList The command line argument list.
438    */
439   private static void startGraphicalUserInterface(File commandLineFile, List<String> argList) {
440     logger.info(" Starting up the graphical user interface.");
441 
442     // Set some Swing Constants.
443     UIManager.put("swing.boldMetal", Boolean.FALSE);
444     setDefaultLookAndFeelDecorated(false);
445 
446     /*
447      Some Mac OS X specific features that help FFX look native. These need
448      to be set before the MainPanel is created.
449     */
450     if (SystemUtils.IS_OS_MAC_OSX) {
451       OSXAdapter.setOSXProperties();
452       try {
453         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
454       } catch (Exception e) {
455         //
456       }
457     }
458 
459     // Initialize the Main frame and Force Field X MainPanel.
460     new Main(commandLineFile, argList);
461   }
462 
463   /** Replace the default console handler with our custom FFX handler. */
464   private static void startLogging() {
465     // Remove all log handlers from the default logger.
466     try {
467       Logger defaultLogger = LogManager.getLogManager().getLogger("");
468       Handler[] defaultHandlers = defaultLogger.getHandlers();
469       for (Handler h : defaultHandlers) {
470         defaultLogger.removeHandler(h);
471       }
472     } catch (Exception e) {
473       String error = e.toString();
474       System.err.println(error);
475     }
476 
477     // Turn off log4j
478     Properties properties = new Properties();
479     properties.setProperty("log4j.threshold", "OFF");
480     properties.setProperty("log4j2.level", "OFF");
481     properties.setProperty("org.apache.logging.log4j.level", "OFF");
482     PropertyConfigurator.configure(properties);
483 
484     Logger ffxLogger = Logger.getLogger("ffx");
485     // Remove any existing handlers.
486     for (Handler handler : ffxLogger.getHandlers()) {
487       ffxLogger.removeHandler(handler);
488     }
489     
490     // Retrieve the log level from the ffx.log system property.
491     String logLevel = System.getProperty("ffx.log", "info");
492     Level tempLevel;
493     try {
494       tempLevel = Level.parse(logLevel.toUpperCase());
495     } catch (Exception e) {
496       tempLevel = Level.INFO;
497     }
498 
499     Level level = tempLevel;
500     logHandler = new LogHandler();
501     logHandler.setLevel(level);
502     ffxLogger.addHandler(logHandler);
503     ffxLogger.setLevel(level);
504   }
505 
506   /** Start up the Parallel Java communication layer. */
507   private static void startParallelJava(String[] args) {
508 
509     // Try to read the PJ configuration file.
510     try {
511       configuration = new Configuration("cluster.txt");
512     } catch (IOException e) {
513       // No cluster configuration file was found.
514     }
515 
516     // Attempt to read the "pj.nn" System property.
517     String numNodes = System.getProperty("pj.nn");
518     int requestedNodes = -1;
519     if (numNodes != null) {
520       try {
521         requestedNodes = Integer.parseInt(numNodes);
522       } catch (Exception e) {
523         // Error parsing the the pj.nn property.
524       }
525     }
526 
527     // Attempt to read the "pj.port" System property.
528     String portString = System.getProperty("pj.port");
529     schedulerPort = -1;
530     if (portString != null) {
531       try {
532         schedulerPort = Integer.parseInt(portString);
533       } catch (Exception e) {
534         // Error parsing the the pj.nn property.
535       }
536     }
537 
538     // Configure the scheduler port if it wasn't set on the command line.
539     if (schedulerPort <= 0) {
540       if (requestedNodes <= 0) {
541         // If the "pj.nn" property is not set, configure the port to 1 (no scheduler).
542         schedulerPort = 1;
543       } else if (configuration != null) {
544         // Configure the port using the Configuration.
545         schedulerPort = configuration.getSchedulerPort();
546       } else {
547         // Set the port to the PJ default.
548         schedulerPort = 20617; // Must sync this value with the Scheduler.groovy script.
549       }
550       System.setProperty("pj.port", Integer.toString(schedulerPort));
551     }
552 
553     try {
554       Comm.init(args);
555       world = Comm.world();
556     } catch (Exception e) {
557       // Log the exception to System.err because the logging subsystem has not be initialized yet.
558       String message = " Exception starting up the Parallel Java communication layer.";
559       System.err.println(message);
560       String error = e.toString();
561       System.err.println(error);
562     }
563   }
564 
565   public static Script runScript(ModelingShell shell, File commandLineFile, List<String> argList) {
566     // Attempt to run a supplied script.
567     if (commandLineFile.exists()) {
568       return shell.runFFXScript(commandLineFile, argList);
569     } else {
570       // See if the commandLineFile is an embedded script.
571       String name = commandLineFile.getName();
572       Class<? extends Script> ffxScript = FFXScript.getScript(name);
573       if (ffxScript != null) {
574         return shell.runFFXScript(ffxScript, argList);
575       }
576     }
577     return null;
578   }
579 
580   /**
581    * {@inheritDoc}
582    *
583    * <p>Commons.Lang Style toString.
584    */
585   @Override
586   public String toString() {
587     ToStringBuilder toStringBuilder =
588         new ToStringBuilder(this)
589             .append("Up Time: " + stopWatch)
590             .append("Logger: " + logger.getName());
591     return toStringBuilder.toString();
592   }
593 }