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-2025.
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.FFXCommand;
48  import org.apache.commons.io.FilenameUtils;
49  import org.apache.commons.lang3.SystemUtils;
50  import org.apache.commons.lang3.builder.ToStringBuilder;
51  import org.apache.commons.lang3.time.StopWatch;
52  import org.apache.commons.logging.impl.LogFactoryImpl;
53  
54  import javax.annotation.Nullable;
55  import javax.swing.ImageIcon;
56  import javax.swing.JFrame;
57  import javax.swing.SwingUtilities;
58  import javax.swing.UIManager;
59  import java.awt.GraphicsEnvironment;
60  import java.awt.Toolkit;
61  import java.awt.event.WindowAdapter;
62  import java.awt.event.WindowEvent;
63  import java.io.File;
64  import java.io.IOException;
65  import java.net.InetAddress;
66  import java.net.URL;
67  import java.net.UnknownHostException;
68  import java.util.ArrayList;
69  import java.util.Arrays;
70  import java.util.Date;
71  import java.util.List;
72  import java.util.Properties;
73  import java.util.concurrent.TimeUnit;
74  import java.util.logging.Handler;
75  import java.util.logging.Level;
76  import java.util.logging.LogManager;
77  import java.util.logging.Logger;
78  
79  import static java.lang.String.format;
80  
81  /**
82   * The Main class is the entry point to the graphical user interface version of Force Field X.
83   *
84   * @author Michael J. Schnieders
85   * @since 1.0
86   */
87  public final class Main extends JFrame {
88  
89    private static final Logger logger = Logger.getLogger(Main.class.getName());
90    /**
91     * This is the main application wrapper.
92     */
93    public static MainPanel mainPanel;
94    /**
95     * Constant <code>stopWatch</code>
96     */
97    private static StopWatch stopWatch = new StopWatch();
98    /**
99     * Handle FFX logging.
100    */
101   private static LogHandler logHandler;
102   /**
103    * The configured Scheduler port.
104    */
105   private static int schedulerPort;
106   /**
107    * Parallel Java Configuration instance.
108    */
109   private static Configuration configuration = null;
110   /**
111    * Parallel Java World Communicator.
112    */
113   private static Comm world = null;
114   /**
115    * Name of the machine FFX is running on.
116    */
117   private static String hostName = null;
118   /**
119    * Force Field X process ID.
120    */
121   private static int procID = -1;
122   /**
123    * Print version and exit
124    */
125   private static boolean printVersionAndExit = false;
126   /**
127    * This Runnable is used to init the GUI using SwingUtilities.
128    */
129   private final Runnable initGUI =
130       new Runnable() {
131         // Create the MainPanel and MainMenu, then add them to the JFrame.
132         public void run() {
133 
134           Toolkit.getDefaultToolkit().setDynamicLayout(true);
135 
136           // Set the Title and Icon
137           Main.this.setTitle("Force Field X");
138           URL iconURL = getClass().getClassLoader().getResource("ffx/ui/icons/icon64.png");
139           if (iconURL != null) {
140             ImageIcon icon = new ImageIcon(iconURL);
141             Main.this.setIconImage(icon.getImage());
142           }
143           Main.this.addWindowListener(
144               new WindowAdapter() {
145                 @Override
146                 public void windowClosing(WindowEvent e) {
147                   if (mainPanel != null) {
148                     mainPanel.exit();
149                   }
150                   System.exit(0);
151                 }
152               });
153 
154           mainPanel = new MainPanel(Main.this);
155           mainPanel.setVisible(false);
156           logHandler.setMainPanel(mainPanel);
157           Main.this.add(mainPanel);
158           mainPanel.initialize();
159           Main.this.setVisible(true);
160           mainPanel.setVisible(true);
161           Main.this.setJMenuBar(mainPanel.getMainMenu());
162 
163           // This is a hack to get GraphicsCanvis to initialize on some platform/Java3D
164           // combinations.
165           // mainPanel.setPanel(MainPanel.KEYWORDS);
166           // mainPanel.setVisible(true);
167           // mainPanel.setPanel(MainPanel.GRAPHICS);
168 
169           // Mac OS X specific features that help Force Field X look native
170           // on Macs. This needs to be done after the MainPanel is created.
171           if (SystemUtils.IS_OS_MAC_OSX) {
172             OSXAdapter osxAdapter = new OSXAdapter(mainPanel);
173           }
174 
175           SwingUtilities.updateComponentTreeUI(SwingUtilities.getRoot(Main.this));
176         }
177       };
178 
179   /**
180    * Main does some window initializations.
181    *
182    * @param commandLineFile a {@link java.io.File} object.
183    * @param argList         a {@link java.util.List} object.
184    */
185   public Main(@Nullable File commandLineFile, List<String> argList) {
186     super("Force Field X");
187     // Start the clock.
188     stopWatch.start();
189 
190     // Init the GUI.
191     try {
192       SwingUtilities.invokeAndWait(initGUI);
193     } catch (Exception e) {
194       logger.warning(" Exception initializing the GUI.\n" + e);
195     }
196 
197     // Run the supplied command or file.
198     if (commandLineFile != null) {
199       runScript(mainPanel.getModelingShell(), commandLineFile, argList);
200     }
201 
202     if (logger.isLoggable(Level.FINE)) {
203       StringBuilder sb = new StringBuilder();
204       sb.append(format("\n Start-up Time (msec): %s.", stopWatch.getTime(TimeUnit.MILLISECONDS)));
205       Runtime runtime = Runtime.getRuntime();
206       runtime.gc();
207       long occupiedMemory = runtime.totalMemory() - runtime.freeMemory();
208       long KB = 1024;
209       sb.append(format("\n In-Use Memory   (Kb): %d", occupiedMemory / KB));
210       sb.append(format("\n Free Memory     (Kb): %d", runtime.freeMemory() / KB));
211       sb.append(format("\n Total Memory    (Kb): %d", runtime.totalMemory() / KB));
212       logger.fine(sb.toString());
213     }
214   }
215 
216   /**
217    * Create an instance of Force Field X
218    *
219    * @param args an array of {@link java.lang.String} objects.
220    */
221   public static void main(String[] args) {
222 
223     List<String> argList = new ArrayList<>();
224     File commandLineFile = initMain(args, argList);
225 
226     try {
227       // Start up the GUI or CLI version of Force Field X.
228       if (!GraphicsEnvironment.isHeadless()) {
229         startGraphicalUserInterface(commandLineFile, argList);
230       } else {
231         if (commandLineFile == null) {
232           logger.info(" No command line file supplied.");
233         } else {
234           startCommandLineInterface(commandLineFile, argList);
235         }
236       }
237     } catch (Throwable t) {
238       int statusCode = 1;
239       logger.info(" Uncaught exception: exiting with status code " + statusCode);
240       logger.info(Utilities.stackTraceToString(t));
241       System.exit(statusCode);
242     }
243   }
244 
245   /**
246    * Process the input arguments into a List, start the logging, start Parallel Java and process the
247    * input command.
248    *
249    * @param args    an array of {@link java.lang.String} objects.
250    * @param argList List is filled with processed arguments.
251    * @return A file for FFX to operate on.
252    */
253   public static File initMain(String[] args, List<String> argList) {
254     // Process any "-D" command line flags.
255     args = processProperties(args);
256 
257     // Start up the Parallel Java communication layer.
258     startParallelJava(args);
259 
260     // Configure our logging.
261     startLogging();
262 
263     // Determine host name and process ID.
264     environment();
265 
266     // Print the header.
267     header(args);
268 
269     // Print out help for the command line interface.
270     if (GraphicsEnvironment.isHeadless() && args.length < 2) {
271       if (args.length == 1 && args[0].toUpperCase().contains("TEST")) {
272         commandLineInterfaceHelp(true);
273       }
274       commandLineInterfaceHelp(false);
275     }
276 
277     // Parse the specified command or structure file.
278     File commandLineFile = null;
279     int nArgs = args.length;
280     if (nArgs > 0) {
281       commandLineFile = new File(args[0]);
282       // Resolve a relavtive path
283       if (commandLineFile.exists()) {
284         commandLineFile = new File(FilenameUtils.normalize(commandLineFile.getAbsolutePath()));
285       }
286     }
287 
288     // Convert the args to a List<String>.
289     if (nArgs > 1) {
290       argList.addAll(Arrays.asList(args).subList(1, nArgs));
291     }
292 
293     return commandLineFile;
294   }
295 
296   /**
297    * A main entry point that runs a script and return a refernce to the result.
298    *
299    * @param args an array of {@link java.lang.String} objects.
300    * @return A Groovy Script instance.
301    */
302   public static FFXCommand ffxScript(String[] args) {
303     List<String> argList = new ArrayList<>();
304     File commandLineFile = initMain(args, argList);
305     try {
306       if (commandLineFile == null) {
307         logger.info(" No command line file supplied.");
308       } else {
309         return startCommandLineInterface(commandLineFile, argList);
310       }
311     } catch (Throwable t) {
312       int statusCode = 1;
313       logger.info(" Uncaught exception: exiting with status code " + statusCode);
314       logger.info(Utilities.stackTraceToString(t));
315       System.exit(statusCode);
316     }
317     return null;
318   }
319 
320   /**
321    * Print out help for the command line version of Force Field X.
322    */
323   private static void commandLineInterfaceHelp(boolean listTestScripts) {
324     logger.info(" usage: ffxc [-D<property=value>] <command> [-options] <PDB|XYZ>");
325     if (listTestScripts) {
326       FFXCommand.listCommands(false, true);
327       logger.info("\n For help on an experimental or test command use:  ffxc <command> -h\n");
328     } else {
329       FFXCommand.listCommands(true, false);
330       logger.info("\n To list experimental & test scripts: ffxc --test");
331       logger.info(" For help on a specific command use:  ffxc <command> -h\n");
332     }
333     System.exit(0);
334   }
335 
336   /**
337    * Determine the host name, process ID, and FFX base directory.
338    */
339   private static void environment() {
340     try {
341       InetAddress addr = InetAddress.getLocalHost();
342       hostName = addr.getHostName();
343     } catch (UnknownHostException e) {
344       // Do nothing.
345     }
346 
347     String procString = System.getProperty("app.pid");
348     if (procString != null) {
349       procID = Integer.parseInt(procString);
350     } else {
351       procID = 0;
352     }
353 
354     String dirString = System.getProperty("basedir");
355     File ffxDirectory;
356     if (dirString != null) {
357       ffxDirectory = new File(dirString);
358     } else {
359       ffxDirectory = new File(".");
360     }
361 
362     try {
363       logger.fine(format(" Force Field X directory is %s", ffxDirectory.getCanonicalPath()));
364     } catch (Exception e) {
365       // Do Nothing.
366     }
367   }
368 
369   /**
370    * Print out a promo.
371    */
372   private static void header(String[] args) {
373     StringBuilder sb = new StringBuilder();
374     sb.append(MainPanel.border).append("\n");
375     sb.append(MainPanel.title).append("\n");
376     sb.append(MainPanel.aboutString).append("\n");
377     sb.append(MainPanel.border);
378 
379     // Print the FFX version and exit.
380     if (printVersionAndExit && GraphicsEnvironment.isHeadless()) {
381       logger.info(sb.toString());
382       System.exit(0);
383     }
384 
385     sb.append("\n ").append(new Date());
386     sb.append(format("\n Process ID %d on %s.", procID, hostName));
387 
388     // Print out command line arguments if the array is not null.
389     if (args != null && args.length > 0) {
390       sb.append("\n\n Command line arguments:\n ");
391       sb.append(Arrays.toString(args));
392       sb.append("\n");
393     }
394 
395     if (schedulerPort > 1) {
396       sb.append(format("\n Parallel Java:\n %s", world.toString()));
397       if (configuration != null) {
398         sb.append(
399             format(
400                 "\n Scheduler %s on port %d.\n", configuration.getSchedulerHost(), schedulerPort));
401       }
402     }
403 
404     logger.info(sb.toString());
405   }
406 
407   /**
408    * Process any "-D" command line flags.
409    */
410   private static String[] processProperties(String[] args) {
411     List<String> newArgs = new ArrayList<>();
412     for (String arg : args) {
413       arg = arg.trim();
414 
415       if (arg.equals("-V") || arg.equals("--version")) {
416         printVersionAndExit = true;
417       }
418 
419       if (arg.startsWith("-D")) {
420         // Remove -D from the front of String.
421         arg = arg.substring(2);
422         // Split at the first equals if it exists.
423         if (arg.contains("=")) {
424           int equalsPosition = arg.indexOf("=");
425           String key = arg.substring(0, equalsPosition);
426           String value = arg.substring(equalsPosition + 1);
427           // Set the system property.
428           System.setProperty(key, value);
429         } else {
430           if (!arg.isEmpty()) {
431             System.setProperty(arg, "");
432           }
433         }
434       } else {
435         // Collect non "-D" arguments.
436         newArgs.add(arg);
437       }
438     }
439     // Return the remaining arguments.
440     args = new String[newArgs.size()];
441     newArgs.toArray(args);
442     return args;
443   }
444 
445   /**
446    * Start the Force Field X command line interface.
447    *
448    * @param commandLineFile The command line file.
449    * @param argList         The command line argument list.
450    */
451   private static FFXCommand startCommandLineInterface(File commandLineFile, List<String> argList) {
452     if (configuration == null) {
453       logger.info(" Starting up the command line interface.\n");
454     }
455     HeadlessMain m = new HeadlessMain(logHandler);
456     mainPanel = m.mainPanel;
457     return runScript(mainPanel.getModelingShell(), commandLineFile, argList);
458   }
459 
460   /**
461    * Start the Force Field X graphical user interface.
462    *
463    * @param commandLineFile The command line file.
464    * @param argList         The command line argument list.
465    */
466   private static void startGraphicalUserInterface(File commandLineFile, List<String> argList) {
467     logger.info(" Starting up the graphical user interface.");
468 
469     // Set some Swing Constants.
470     UIManager.put("swing.boldMetal", Boolean.FALSE);
471     setDefaultLookAndFeelDecorated(false);
472 
473     /*
474      Some Mac OS X specific features that help FFX look native. These need
475      to be set before the MainPanel is created.
476     */
477     if (SystemUtils.IS_OS_MAC_OSX) {
478       OSXAdapter.setOSXProperties();
479       try {
480         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
481       } catch (Exception e) {
482         //
483       }
484     }
485 
486     // Initialize the Main frame and Force Field X MainPanel.
487     new Main(commandLineFile, argList);
488   }
489 
490   /**
491    * Replace the default console handler with our custom FFX handler.
492    */
493   private static void startLogging() {
494     // Remove all log handlers from the default logger.
495     try {
496       Logger defaultLogger = LogManager.getLogManager().getLogger("");
497       Handler[] defaultHandlers = defaultLogger.getHandlers();
498       for (Handler h : defaultHandlers) {
499         defaultLogger.removeHandler(h);
500       }
501     } catch (Exception e) {
502       String error = e.toString();
503       System.err.println(error);
504     }
505 
506     // Suppress various loggers used by FFX dependencies.
507     // Attempt to suppress Log4j version 1.x.
508     // https://logging.apache.org/log4j/1.x/manual.html
509     System.setProperty("log4j.threshold", "OFF");
510     System.setProperty("log4j.rootLogger", "OFF");
511     System.setProperty("log4j1.compatibility", "true");
512     Properties properties = new Properties();
513     properties.setProperty("log4j.threshold", "OFF");
514     properties.setProperty("log4j.rootLogger", "OFF");
515     properties.setProperty("log4j1.compatibility", "true");
516     org.apache.log4j.PropertyConfigurator.configure(properties);
517     org.apache.log4j.LogManager.getRootLogger().setLevel(org.apache.log4j.Level.OFF);
518 
519     // Attempt to suppress Log4j2 version 2.x.
520     // https://logging.apache.org/log4j/2.x/manual
521     System.setProperty("log4j2.level", "OFF");
522     System.setProperty("log4j2.simplelogLevel", "OFF");
523     System.setProperty("log4j2.disable.jmx", "true");
524     org.apache.logging.log4j.LogManager.getRootLogger().atLevel(org.apache.logging.log4j.Level.OFF);
525 
526     // Attempt to suppress SLF4J loggers.
527     // https://www.slf4j.org/manual.html
528     System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "off");
529     // The statement below causes a warning to be printed to the console for SLF4J 2.0.16.
530     // System.setProperty(LoggerFactory.PROVIDER_PROPERTY_KEY, "org.slf4j.nop.NOPServiceProvider");
531     // org.slf4j.Logger rootLogger = org.slf4j.LoggerFactory.getILoggerFactory().getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
532 
533     // Attempt to suppress Apache Commons loggers.
534     // https://commons.apache.org/proper/commons-logging/guide.html
535     System.setProperty(LogFactoryImpl.LOG_PROPERTY, "org.apache.commons.logging.impl.NoOpLog");
536     org.apache.commons.logging.LogFactory.getFactory()
537         .setAttribute(LogFactoryImpl.LOG_PROPERTY, "org.apache.commons.logging.impl.NoOpLog");
538 
539     /*
540     // The following message is logged in Jupyter notebooks. It would be nice to determine how to suppress it.
541     // The bug is described here: https://issues.apache.org/jira/browse/BEANUTILS-477
542     // INFO org.apache.commons.beanutils.FluentPropertyBeanIntrospector -
543     // Error when creating PropertyDescriptor for
544     // public final void org.apache.commons.configuration2.AbstractConfiguration.setProperty(java.lang.String,java.lang.Object)!
545     // Ignoring this property.
546      */
547 
548     Logger ffxLogger = Logger.getLogger("ffx");
549     // Remove any existing handlers.
550     for (Handler handler : ffxLogger.getHandlers()) {
551       ffxLogger.removeHandler(handler);
552     }
553 
554     // Retrieve the log level from the ffx.log system property.
555     String logLevel = System.getProperty("ffx.log", "info");
556     Level tempLevel;
557     try {
558       tempLevel = Level.parse(logLevel.toUpperCase());
559     } catch (Exception e) {
560       tempLevel = Level.INFO;
561     }
562 
563     Level level = tempLevel;
564     logHandler = new LogHandler();
565     logHandler.setLevel(level);
566     ffxLogger.addHandler(logHandler);
567     ffxLogger.setLevel(level);
568   }
569 
570   /**
571    * Start up the Parallel Java communication layer.
572    */
573   private static void startParallelJava(String[] args) {
574 
575     // Try to read the PJ configuration file.
576     try {
577       configuration = new Configuration("cluster.txt");
578     } catch (IOException e) {
579       // No cluster configuration file was found.
580     }
581 
582     // Attempt to read the "pj.nn" System property.
583     String numNodes = System.getProperty("pj.nn");
584     int requestedNodes = -1;
585     if (numNodes != null) {
586       try {
587         requestedNodes = Integer.parseInt(numNodes);
588       } catch (Exception e) {
589         // Error parsing the the pj.nn property.
590       }
591     }
592 
593     // Attempt to read the "pj.port" System property.
594     String portString = System.getProperty("pj.port");
595     schedulerPort = -1;
596     if (portString != null) {
597       try {
598         schedulerPort = Integer.parseInt(portString);
599       } catch (Exception e) {
600         // Error parsing the the pj.nn property.
601       }
602     }
603 
604     // Configure the scheduler port if it wasn't set on the command line.
605     if (schedulerPort <= 0) {
606       if (requestedNodes <= 0) {
607         // If the "pj.nn" property is not set, configure the port to 1 (no scheduler).
608         schedulerPort = 1;
609       } else if (configuration != null) {
610         // Configure the port using the Configuration.
611         schedulerPort = configuration.getSchedulerPort();
612       } else {
613         // Set the port to the PJ default.
614         schedulerPort = 20617; // Must sync this value with the Scheduler.groovy script.
615       }
616       System.setProperty("pj.port", Integer.toString(schedulerPort));
617     }
618 
619     try {
620       Comm.init(args);
621       world = Comm.world();
622     } catch (Exception e) {
623       // Log the exception to System.err because the logging subsystem has not be initialized yet.
624       String message = " Exception starting up the Parallel Java communication layer.";
625       System.err.println(message);
626       String error = e.toString();
627       System.err.println(error);
628     }
629   }
630 
631   public static FFXCommand runScript(ModelingShell shell, File commandLineFile, List<String> argList) {
632     // Attempt to run a supplied script.
633     if (commandLineFile.exists()) {
634       return shell.runFFXScript(commandLineFile, argList);
635     } else {
636       // See if the commandLineFile is an embedded script.
637       String name = commandLineFile.getName();
638       Class<? extends FFXCommand> command = FFXCommand.getCommand(name);
639       if (command != null) {
640         return shell.runFFXScript(command, argList);
641       }
642     }
643     return null;
644   }
645 
646   /**
647    * {@inheritDoc}
648    *
649    * <p>Commons.Lang Style toString.
650    */
651   @Override
652   public String toString() {
653     ToStringBuilder toStringBuilder =
654         new ToStringBuilder(this)
655             .append("Up Time: " + stopWatch)
656             .append("Logger: " + logger.getName());
657     return toStringBuilder.toString();
658   }
659 }