001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028package edu.wisc.ssec.mcidasv.util;
029
030import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
031import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashMap;
032
033import java.io.BufferedReader;
034import java.io.File;
035import java.io.FileNotFoundException;
036import java.io.IOException;
037import java.io.InputStream;
038import java.io.InputStreamReader;
039import java.lang.management.ManagementFactory;
040import java.lang.management.OperatingSystemMXBean;
041
042import java.net.URL;
043import java.nio.file.Path;
044import java.nio.file.Paths;
045import java.text.ParseException;
046import java.text.SimpleDateFormat;
047import java.util.Date;
048import java.util.Enumeration;
049import java.util.HashMap;
050import java.util.List;
051import java.util.Map;
052import java.util.Properties;
053import java.util.TimeZone;
054import java.util.TreeSet;
055
056import java.awt.DisplayMode;
057import java.awt.GraphicsConfiguration;
058import java.awt.GraphicsDevice;
059import java.awt.GraphicsEnvironment;
060import java.awt.Rectangle;
061import java.util.jar.Attributes;
062import java.util.jar.Manifest;
063
064import javax.management.MBeanServer;
065import javax.management.ObjectName;
066import javax.media.j3d.Canvas3D;
067import javax.media.j3d.VirtualUniverse;
068
069import com.google.common.base.Splitter;
070import org.python.core.Py;
071import org.python.core.PySystemState;
072
073import org.python.modules.posix.PosixModule;
074import org.slf4j.Logger;
075import org.slf4j.LoggerFactory;
076
077import ucar.nc2.grib.GribConverterUtility;
078import ucar.unidata.idv.ArgsManager;
079import ucar.unidata.idv.IdvResourceManager.IdvResource;
080import ucar.unidata.util.IOUtil;
081import ucar.unidata.util.ResourceCollection;
082import ucar.visad.display.DisplayUtil;
083
084import edu.wisc.ssec.mcidasv.Constants;
085import edu.wisc.ssec.mcidasv.McIDASV;
086import edu.wisc.ssec.mcidasv.StateManager;
087
088/**
089 * Utility methods for querying the state of the user's machine.
090 */
091public class SystemState {
092    
093    /** Handy logging object. */
094    private static final Logger logger = 
095        LoggerFactory.getLogger(SystemState.class);
096        
097    // Don't allow outside instantiation.
098    private SystemState() { }
099    
100    public static String escapeWhitespaceChars(final CharSequence sequence) {
101        StringBuilder sb = new StringBuilder(sequence.length() * 7);
102        for (int i = 0; i < sequence.length(); i++) {
103            switch (sequence.charAt(i)) {
104                case '\t': sb.append("\\t"); break;
105                case '\n': sb.append('\\').append('n'); break;
106                case '\013': sb.append("\\013"); break;
107                case '\f': sb.append("\\f"); break;
108                case '\r': sb.append("\\r"); break;
109                case '\u0085': sb.append("\\u0085"); break;
110                case '\u1680': sb.append("\\u1680"); break;
111                case '\u2028': sb.append("\\u2028"); break;
112                case '\u2029': sb.append("\\u2029"); break;
113                case '\u205f': sb.append("\\u205f"); break;
114                case '\u3000': sb.append("\\u3000"); break;
115            }
116        }
117        return sb.toString();
118    }
119    
120    /**
121     * Query an {@link OperatingSystemMXBean} attribute and return the result.
122     *
123     * @param <T> Type of the expected return value and {@code defaultValue}.
124     * @param attrName Name of the {@code OperatingSystemMXBean} attribute to 
125     *                 query. Cannot be {@code null} or empty.
126     * @param defaultValue Value returned if {@code attrName} could not be 
127     *                     queried.
128     *
129     * @return Either the value corresponding to {@code attrName} or 
130     *         {@code defaultValue}.
131     */
132    private static <T> T queryPlatformBean(final String attrName,
133                                           final T defaultValue)
134    {
135        assert attrName != null : "Cannot query a null attribute name";
136        assert !attrName.isEmpty() : "Cannot query an empty attribute name";
137        T result = defaultValue;
138        try {
139            final ObjectName objName = 
140                new ObjectName("java.lang", "type", "OperatingSystem");
141            final MBeanServer beanServer = 
142                ManagementFactory.getPlatformMBeanServer();
143            final Object attr = beanServer.getAttribute(objName, attrName);
144            if (attr != null) {
145                // don't suppress warnings because we cannot guarantee that 
146                // this cast is correct.
147                result = (T)attr;
148            }
149        } catch (Exception e) {
150            logger.error("Couldn't query attribute: " + attrName, e);
151        }
152        return result;
153    }
154    
155    /**
156     * Returns the contents of Jython's registry (basically just Jython-specific
157     * properties) as well as some of the information from Python's 
158     * {@literal "sys"} module. 
159     * 
160     * @return Jython's configuration settings. 
161     */
162    public static Map<Object, Object> queryJythonProps() {
163        Map<Object, Object> properties = newLinkedHashMap(PySystemState.registry);
164        try (PySystemState systemState = Py.getSystemState()) {
165            properties.put("sys.argv", systemState.argv.toString());
166            properties.put("sys.path", systemState.path);
167            properties.put("sys.platform", systemState.platform.toString());
168        }
169        properties.put("sys.builtin_module_names", PySystemState.builtin_module_names.toString());
170        properties.put("sys.byteorder", PySystemState.byteorder);
171        properties.put("sys.isPackageCacheEnabled", PySystemState.isPackageCacheEnabled());
172        properties.put("sys.version", PySystemState.version);
173        properties.put("sys.version_info", PySystemState.version_info);
174        return properties;
175    }
176    
177    /**
178     * Attempts to call methods belonging to 
179     * {@code com.sun.management.OperatingSystemMXBean}. If successful, we'll
180     * have the following information:
181     * <ul>
182     *   <li>opsys.memory.virtual.committed: virtual memory that is guaranteed to be available</li>
183     *   <li>opsys.memory.swap.total: total amount of swap space in bytes</li>
184     *   <li>opsys.memory.swap.free: free swap space in bytes</li>
185     *   <li>opsys.cpu.time: CPU time used by the process (nanoseconds)</li>
186     *   <li>opsys.memory.physical.free: free physical memory in bytes</li>
187     *   <li>opsys.memory.physical.total: physical memory in bytes</li>
188     *   <li>opsys.load: system load average for the last minute</li>
189     * </ul>
190     * 
191     * @return Map of properties that contains interesting information about
192     * the hardware McIDAS-V is using.
193     */
194    public static Map<String, String> queryOpSysProps() {
195        Map<String, String> properties = newLinkedHashMap(10);
196        long committed = queryPlatformBean("CommittedVirtualMemorySize", Long.MIN_VALUE);
197        long freeMemory = queryPlatformBean("FreePhysicalMemorySize", Long.MIN_VALUE);
198        long freeSwap = queryPlatformBean("FreeSwapSpaceSize", Long.MIN_VALUE);
199        long cpuTime = queryPlatformBean("ProcessCpuTime", Long.MIN_VALUE);
200        long totalMemory = queryPlatformBean("TotalPhysicalMemorySize", Long.MIN_VALUE);
201        long totalSwap = queryPlatformBean("TotalSwapSpaceSize", Long.MIN_VALUE);
202        double loadAvg = queryPlatformBean("SystemLoadAverage", Double.NaN);
203        
204        Runtime rt = Runtime.getRuntime();
205        long currentMem = rt.totalMemory() - rt.freeMemory();
206        
207        properties.put("opsys.cpu.time", Long.toString(cpuTime));
208        properties.put("opsys.load", Double.toString(loadAvg));
209        properties.put("opsys.memory.jvm.current", Long.toString(currentMem));
210        properties.put("opsys.memory.jvm.max", Long.toString(rt.maxMemory()));
211        properties.put("opsys.memory.virtual.committed", Long.toString(committed));
212        properties.put("opsys.memory.physical.free", Long.toString(freeMemory));
213        properties.put("opsys.memory.physical.total", Long.toString(totalMemory));
214        properties.put("opsys.memory.swap.free", Long.toString(freeSwap));
215        properties.put("opsys.memory.swap.total", Long.toString(totalSwap));
216        
217        return properties;
218    }
219    
220    /**
221     * Polls Java for information about the user's machine. We're specifically
222     * after memory statistics, number of processors, and display information.
223     * 
224     * @return {@link Map} of properties that describes the user's machine.
225     */
226    public static Map<String, String> queryMachine() {
227        Map<String, String> props = newLinkedHashMap();
228        
229        // cpu count and whatnot
230        int processors = Runtime.getRuntime().availableProcessors();
231        props.put("opsys.cpu.count", Integer.toString(processors));
232        
233        // memory: available, used, etc
234        props.putAll(queryOpSysProps());
235        
236        // screen: count, resolution(s)
237        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
238        int displayCount = ge.getScreenDevices().length;
239        
240        for (int i = 0; i < displayCount; i++) {
241            String baseId = "opsys.display."+i+'.';
242            GraphicsDevice dev = ge.getScreenDevices()[i];
243            DisplayMode mode = dev.getDisplayMode();
244            props.put(baseId+"name", dev.getIDstring());
245            props.put(baseId+"depth", Integer.toString(mode.getBitDepth()));
246            props.put(baseId+"width", Integer.toString(mode.getWidth()));
247            props.put(baseId+"height", Integer.toString(mode.getHeight()));
248            props.put(baseId+"refresh", Integer.toString(mode.getRefreshRate()));
249        }
250        return props;
251    }
252    
253    /**
254     * Returns a mapping of display number to a {@link java.awt.Rectangle} 
255     * that represents the {@literal "bounds"} of the display.
256     *
257     * @return Rectangles representing the {@literal "bounds"} of the current
258     * display devices.
259     */
260    public static Map<Integer, Rectangle> getDisplayBounds() {
261        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
262        int idx = 0;
263        Map<Integer, Rectangle> map = newLinkedHashMap(ge.getScreenDevices().length * 2);
264        for (GraphicsDevice dev : ge.getScreenDevices()) {
265            for (GraphicsConfiguration config : dev.getConfigurations()) {
266                map.put(idx++, config.getBounds());
267            }
268        }
269        return map;
270    }
271    
272    // TODO(jon): this should really be a polygon
273    public static Rectangle getVirtualDisplayBounds() {
274        Rectangle virtualBounds = new Rectangle();
275        for (Rectangle bounds : getDisplayBounds().values()) {
276            virtualBounds = virtualBounds.union(bounds);
277        }
278        return virtualBounds;
279    }
280    
281    /**
282     * Polls Java 3D for information about its environment. Specifically, we 
283     * call {@link VirtualUniverse#getProperties()} and 
284     * {@link Canvas3D#queryProperties()}.
285     * 
286     * @return As much information as Java 3D can provide.
287     */
288    @SuppressWarnings("unchecked") // casting to Object, so this should be fine.
289    public static Map<String, Object> queryJava3d() {
290        
291        Map<String, Object> universeProps = 
292            (Map<String, Object>)VirtualUniverse.getProperties();
293            
294        GraphicsConfiguration config =
295            DisplayUtil.getPreferredConfig(null, true, false);
296        Map<String, Object> c3dMap = new Canvas3D(config).queryProperties();
297        
298        Map<String, Object> props =
299                newLinkedHashMap(universeProps.size() + c3dMap.size());
300        props.putAll(universeProps);
301        props.putAll(c3dMap);
302        return props;
303    }
304    
305    /**
306     * Gets a human-friendly representation of the information embedded within
307     * IDV's {@code build.properties}.
308     *
309     * @return {@code String} that looks like {@literal "IDV version major.minor<b>revision</b> built <b>date</b>"}.
310     * For example: {@code IDV version 2.9u4 built 2011-04-13 14:01 UTC}.
311     */
312    public static String getIdvVersionString() {
313        Map<String, String> info = queryIdvBuildProperties();
314        return "IDV version " + info.get("idv.version.major") + '.' +
315               info.get("idv.version.minor") + info.get("idv.version.revision") +
316               " built " + info.get("idv.build.date");
317    }
318    
319    /**
320     * Gets a human-friendly representation of the information embedded within
321     * McIDAS-V's {@code build.properties}.
322     * 
323     * @return {@code String} that looks like {@literal "McIDAS-V version major.minor<b>release</b> built <b>date</b>"}.
324     * For example: {@code McIDAS-V version 1.02beta1 built 2011-04-14 17:36}.
325     */
326    public static String getMcvVersionString() {
327        Map<String, String> info = queryMcvBuildProperties();
328        return "McIDAS-V version " + info.get(Constants.PROP_VERSION_MAJOR) + '.' +
329               info.get(Constants.PROP_VERSION_MINOR) + info.get(Constants.PROP_VERSION_RELEASE) +
330               " built " + info.get(Constants.PROP_BUILD_DATE);
331    }
332    
333    /**
334     * Gets a human-friendly representation of the version information embedded 
335     * within VisAD's {@literal "DATE"} file.
336     * 
337     * @return {@code String} that looks
338     * {@literal "VisAD version <b>revision</b> built <b>date</b>"}.
339     * For example: {@code VisAD version 5952 built Thu Mar 22 13:01:31 CDT 2012}.
340     */
341    public static String getVisadVersionString() {
342        Map<String, String> props = queryVisadBuildProperties();
343        return "VisAD version " + props.get(Constants.PROP_VISAD_REVISION) + " built " + props.get(Constants.PROP_VISAD_DATE);
344    }
345    
346    /**
347     * Gets a human-friendly representation of the ncIdv.jar version
348     * information.
349     *
350     * @return {@code String} that looks like
351     * {@literal "netCDF-Java version <b>revision</b> <b>built</b>"}.
352     */
353    public static String getNcidvVersionString() {
354        Map<String, String> props = queryNcidvBuildProperties();
355        return "netCDF-Java version " + props.get("version") + " built " + props.get("buildDate");
356    }
357    
358    /**
359     * Open a file for reading.
360     *
361     * @param name File to open.
362     *
363     * @return {@code InputStream} used to read {@code name}, or {@code null}
364     * if {@code name} could not be found.
365     */
366    private static InputStream resourceAsStream(final String name) {
367        return ClassLoader.getSystemResourceAsStream(name);
368    }
369    
370    /**
371     * Returns a {@link Map} containing any relevant version information. 
372     * 
373     * <p>Currently this information consists of the date visad.jar was built, 
374     * as well as the (then-current) Subversion revision number.
375     * 
376     * @return {@code Map} of the contents of VisAD's DATE file.
377     */
378    public static Map<String, String> queryVisadBuildProperties() {
379        Map<String, String> props = newLinkedHashMap(4);
380        
381        try (BufferedReader input = new BufferedReader(
382                                        new InputStreamReader(
383                                            resourceAsStream("DATE")))) 
384        {
385            String contents = input.readLine();
386            // string should look like: Thu Mar 22 13:01:31 CDT 2012  Rev:5952
387            String splitAt = "  Rev:";
388            int index = contents.indexOf(splitAt);
389            String buildDate = "ERROR";
390            String revision = "ERROR";
391            String parseFail = "true";
392            if (index > 0) {
393                buildDate = contents.substring(0, index);
394                revision = contents.substring(index + splitAt.length());
395                parseFail = "false";
396            }
397            props.put(Constants.PROP_VISAD_ORIGINAL, contents);
398            props.put(Constants.PROP_VISAD_PARSE_FAIL, parseFail);
399            props.put(Constants.PROP_VISAD_DATE, buildDate);
400            props.put(Constants.PROP_VISAD_REVISION, revision);
401        } catch (IOException e) {
402            logger.error("could not read from VisAD DATE file", e);
403        }
404        return props;
405    }
406    
407    /**
408     * Determine the actual name of the McIDAS-V JAR file.
409     *
410     * <p>This is needed because we're now following the Maven naming
411     * convention, and this means that {@literal "mcidasv.jar"} no longer
412     * exists.</p>
413     *
414     * @param jarDir Directory containing all of the McIDAS-V JARs.
415     *               Cannot be {@code null}.
416     *
417     * @return Name (note: not path) of the McIDAS-V JAR file.
418     *
419     * @throws IOException if there was a problem locating the McIDAS-V JAR.
420     */
421    private static String getMcvJarname(String jarDir)
422        throws IOException
423    {
424        File dir = new File(jarDir);
425        File[] files = dir.listFiles();
426        if (files == null) {
427            throw new IOException("Could not get list of files within " +
428                "'"+jarDir+"'");
429        }
430        for (File f : files) {
431            String name = f.getName();
432            if (name.startsWith("mcidasv-") && name.endsWith(".jar")) {
433                return name;
434            }
435        }
436        throw new FileNotFoundException("Could not find McIDAS-V JAR file");
437    }
438    
439    /**
440     * Return McIDAS-V's classpath.
441     *
442     * <p>This may differ from what is reported by 
443     * {@code System.getProperty("java.class.path")}. This is because McIDAS-V
444     * specifies its classpath using {@code META-INF/MANIFEST.MF} in 
445     * {@code mcidasv.jar}.
446     * </p>
447     *
448     * <p>This is used by console_init.py to figure out where the VisAD,
449     * IDV, and McIDAS-V JAR files are located.</p>
450     *
451     * @return Either a list of strings containing the path to each JAR file
452     * in the classpath, or an empty list.
453     */
454    public static List<String> getMcvJarClasspath() {
455        String jarDir = System.getProperty("user.dir");
456        if (jarDir == null) {
457            jarDir = PosixModule.getcwd().toString();
458        }
459
460        // 64 chosen because we're currently at 38 JARs.
461        List<String> jars = arrList(64);
462        try {
463            String mcvJar = getMcvJarname(jarDir);
464            Path p = Paths.get(jarDir, mcvJar);
465            String path = "jar:file:"+p.toString()+"!/META-INF/MANIFEST.MF";
466            InputStream stream = IOUtil.getInputStream(path);
467            if (stream != null) {
468                Manifest manifest = new Manifest(stream);
469                Attributes attrs = manifest.getMainAttributes();
470                if (attrs != null) {
471                    String classpath = attrs.getValue("Class-Path");
472                    Splitter split = Splitter.on(' ')
473                        .trimResults()
474                        .omitEmptyStrings();
475                    for (String jar : split.split(classpath)) {
476                        jars.add(Paths.get(jarDir, jar).toFile().toString());
477                    }
478                }
479            }
480        } catch (IOException | NullPointerException e) {
481            logger.warn("Exception occurred:", e);
482        }
483        return jars;
484    }
485    
486    /**
487     * Returns a {@link Map} containing {@code version} and {@code buildDate}
488     * keys.
489     *
490     * @return {@code Map} containing netCDF-Java version and build date.
491     */
492    public static Map<String, String> queryNcidvBuildProperties() {
493        // largely taken from LibVersionUtil's getBuildInfo method.
494        GribConverterUtility util = new GribConverterUtility();
495        Map<String, String> buildInfo = new HashMap<>(4);
496        // format from ncidv.jar
497        SimpleDateFormat formatIn =
498            new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
499        // format of output timestamp
500        SimpleDateFormat formatOut =
501            new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ");
502        formatOut.setTimeZone(TimeZone.getTimeZone("UTC"));
503        try {
504            Enumeration<URL> resources = 
505                util.getClass()
506                    .getClassLoader()
507                    .getResources("META-INF/MANIFEST.MF");
508                    
509            while (resources.hasMoreElements()) {
510                URL resource = resources.nextElement();
511                if (!resource.toString().contains("ncIdv")) {
512                    // skip over all of the JARs in the classpath except for ncIdv.
513                    continue;
514                }
515                Manifest manifest =
516                    new Manifest(resource.openStream());
517                Attributes attrs = manifest.getMainAttributes();
518                if (attrs != null) {
519                    buildInfo.put(
520                        "version",
521                        attrs.getValue("Implementation-Version"));
522                    String strDate = attrs.getValue("Built-On");
523                    Date date = formatIn.parse(strDate);
524                    buildInfo.put("buildDate", formatOut.format(date));
525                    break;
526                }
527            }
528        } catch (IOException e) {
529            logger.warn("exception occurred:", e);
530        } catch (ParseException pe) {
531            logger.warn("failed to parse build date from ncIdv.jar", pe);
532        }
533        return buildInfo;
534    }
535    
536    /**
537     * Returns a {@link Map} of the (currently) most useful contents of
538     * {@code ucar/unidata/idv/resources/build.properties}.
539     *
540     * <p>Consider the output of {@link #getIdvVersionString()}; it's built
541     * with the the following:
542     * <ul>
543     *   <li><b>{@code idv.version.major}</b>: currently {@literal "3"}</li>
544     *   <li><b>{@code idv.version.minor}</b>: currently {@literal "0"}</li>
545     *   <li><b>{@code idv.version.revision}</b>: currently {@literal "u2"}}</li>
546     *   <li><b>{@code idv.build.date}</b>: varies pretty frequently,
547     *   as it's the build timestamp for idv.jar</li>
548     * </ul>
549     *
550     * @return A {@code Map} of at least the useful parts of build.properties.
551     */
552    public static Map<String, String> queryIdvBuildProperties() {
553        Map<String, String> versions = newLinkedHashMap(4);
554        InputStream input = null;
555        try {
556            input = resourceAsStream("ucar/unidata/idv/resources/build.properties");
557            Properties props = new Properties();
558            props.load(input);
559            String major = props.getProperty("idv.version.major", "no_major");
560            String minor = props.getProperty("idv.version.minor", "no_minor");
561            String revision = props.getProperty("idv.version.revision", "no_revision");
562            String date = props.getProperty("idv.build.date", "");
563            versions.put("idv.version.major", major);
564            versions.put("idv.version.minor", minor);
565            versions.put("idv.version.revision", revision);
566            versions.put("idv.build.date", date);
567        } catch (Exception e) {
568            logger.error("could not read from IDV build.properties", e);
569        } finally {
570            if (input != null) {
571                try {
572                    input.close();
573                } catch (Exception ex) {
574                    logger.error("could not close IDV build.properties", ex);
575                }
576            }
577        }
578        return versions;
579    }
580    
581    /**
582     * Returns a {@link Map} of the (currently) most useful contents of
583     * {@code edu/wisc/ssec/mcidasv/resources/build.properties}.
584     *
585     * <p>Consider the output of {@link #getMcvVersionString()}; it's built
586     * with the the following:
587     * <ul>
588     *   <li><b>{@code mcidasv.version.major}</b>:
589     *   currently {@literal "1"}</li>
590     *   <li><b>{@code mcidasv.version.minor}</b>:
591     *   currently {@literal "02"}</li>
592     *   <li><b>{@code mcidasv.version.release}</b>: currently
593     *   {@literal "beta1"}</li>
594     *   <li><b>{@code mcidasv.build.date}</b>: varies pretty frequently, as
595     *   it's the build timestamp for mcidasv.jar.</li>
596     * </ul>
597     *
598     * @return A {@code Map} of at least the useful parts of build.properties.
599     */
600    public static Map<String, String> queryMcvBuildProperties() {
601        Map<String, String> versions = newLinkedHashMap(4);
602        InputStream input = null;
603        try {
604            input = resourceAsStream("edu/wisc/ssec/mcidasv/resources/build.properties");
605            Properties props = new Properties();
606            props.load(input);
607            String major = props.getProperty(Constants.PROP_VERSION_MAJOR, "0");
608            String minor = props.getProperty(Constants.PROP_VERSION_MINOR, "0");
609            String release = props.getProperty(Constants.PROP_VERSION_RELEASE, "");
610            String date = props.getProperty(Constants.PROP_BUILD_DATE, "Unknown");
611            versions.put(Constants.PROP_VERSION_MAJOR, major);
612            versions.put(Constants.PROP_VERSION_MINOR, minor);
613            versions.put(Constants.PROP_VERSION_RELEASE, release);
614            versions.put(Constants.PROP_BUILD_DATE, date);
615        } catch (Exception e) {
616            logger.error("could not read from McIDAS-V build.properties!", e);
617        } finally {
618            if (input != null) {
619                try {
620                    input.close();
621                } catch (Exception ex) {
622                    logger.error("could not close McIDAS-V build.properties!", ex);
623                }
624            }
625        }
626        return versions;
627    }
628    
629    /**
630     * Queries McIDAS-V for information about its state. There's not a good way
631     * to characterize what we're interested in, so let's leave it at 
632     * {@literal "whatever seems useful"}.
633     * 
634     * @param mcv The McIDASV {@literal "god"} object.
635     * 
636     * @return Information about the state of McIDAS-V.
637     */
638    // need: argsmanager, resource manager
639    public static Map<String, Object> queryMcvState(final McIDASV mcv) {
640        // through some simple verification, props generally has under 250 elements
641        Map<String, Object> props = newLinkedHashMap(250);
642        
643        ArgsManager args = mcv.getArgsManager();
644        props.put("mcv.state.islinteractive", args.getIslInteractive());
645        props.put("mcv.state.offscreen", args.getIsOffScreen());
646        props.put("mcv.state.initcatalogs", args.getInitCatalogs());
647        props.put("mcv.state.actions", mcv.getActionHistory());
648        props.put("mcv.plugins.installed", args.installPlugins);
649        props.put("mcv.state.commandline", mcv.getCommandLineArgs());
650        
651        // loop through resources
652        List<IdvResource> resources =
653                (List<IdvResource>)mcv.getResourceManager().getResources();
654        for (IdvResource resource : resources) {
655            String id = resource.getId();
656            props.put(id+".description", resource.getDescription());
657            if (resource.getPattern() == null) {
658                props.put(id+".pattern", "null");
659            } else {
660                props.put(id+".pattern", resource.getPattern());
661            }
662            
663            ResourceCollection rc = 
664                mcv.getResourceManager().getResources(resource);
665            int rcSize = rc.size();
666            List<String> specified = arrList(rcSize);
667            List<String> valid = arrList(rcSize);
668            for (int i = 0; i < rcSize; i++) {
669                String tmpResource = (String)rc.get(i);
670                specified.add(tmpResource);
671                if (rc.isValid(i)) {
672                    valid.add(tmpResource);
673                }
674            }
675            
676            props.put(id+".specified", specified);
677            props.put(id+".existing", valid);
678        }
679        return props;
680    }
681    
682    /**
683     * Builds a (filtered) subset of the McIDAS-V system properties and returns
684     * the results as a {@code String}.
685     * 
686     * @param mcv The McIDASV {@literal "god"} object.
687     * 
688     * @return The McIDAS-V system properties in the following format: 
689     * {@code KEY=VALUE\n}. This is so we kinda-sorta conform to the standard
690     * {@link Properties} file format.
691     * 
692     * @see #getStateAsString(edu.wisc.ssec.mcidasv.McIDASV, boolean)
693     */
694    public static String getStateAsString(final McIDASV mcv) {
695        return getStateAsString(mcv, false);
696    }
697    
698    /**
699     * Builds the McIDAS-V system properties and returns the results as a 
700     * {@code String}.
701     * 
702     * @param mcv The McIDASV {@literal "god"} object.
703     * @param firehose If {@code true}, enables {@literal "unfiltered"} output.
704     * 
705     * @return The McIDAS-V system properties in the following format: 
706     * {@code KEY=VALUE\n}. This is so we kinda-sorta conform to the standard
707     * {@link Properties} file format.
708     */
709    public static String getStateAsString(final McIDASV mcv, final boolean firehose) {
710        int builderSize = firehose ? 45000 : 1000;
711        StringBuilder buf = new StringBuilder(builderSize);
712        
713        Map<String, String> versions = ((StateManager)mcv.getStateManager()).getVersionInfo();
714        Properties sysProps = System.getProperties();
715        Map<String, Object> j3dProps = queryJava3d();
716        Map<String, String> machineProps = queryMachine();
717        Map<Object, Object> jythonProps = queryJythonProps();
718        Map<String, Object> mcvProps = queryMcvState(mcv);
719        
720        if (sysProps.contains("line.separator")) {
721            sysProps.put("line.separator", escapeWhitespaceChars((String)sysProps.get("line.separator")));
722        }
723        
724        String maxMem = Long.toString(Long.valueOf(machineProps.get("opsys.memory.jvm.max")) / 1048576L);
725        String curMem = Long.toString(Long.valueOf(machineProps.get("opsys.memory.jvm.current")) / 1048576L);
726        
727        buf.append("# Software Versions:")
728            .append("\n# McIDAS-V:    ").append(versions.get("mcv.version.general")).append(" (").append(versions.get("mcv.version.build")).append(')')
729            .append("\n# VisAD:       ").append(versions.get("visad.version.general")).append(" (").append(versions.get("visad.version.build")).append(')')
730            .append("\n# IDV:         ").append(versions.get("idv.version.general")).append(" (").append(versions.get("idv.version.build")).append(')')
731            .append("\n# netcdf-Java: ").append(versions.get("netcdf.version.general")).append(" (").append(versions.get("netcdf.version.build")).append(')')
732            .append("\n\n# Operating System:")
733            .append("\n# Name:         ").append(sysProps.getProperty("os.name"))
734            .append("\n# Version:      ").append(sysProps.getProperty("os.version"))
735            .append("\n# Architecture: ").append(sysProps.getProperty("os.arch"))
736            .append("\n\n# Java:")
737            .append("\n# Version: ").append(sysProps.getProperty("java.version"))
738            .append("\n# Vendor:  ").append(sysProps.getProperty("java.vendor"))
739            .append("\n# Home:    ").append(sysProps.getProperty("java.home"))
740            .append("\n\n# JVM Memory")
741            .append("\n# Current: ").append(curMem).append(" MB")
742            .append("\n# Maximum: ").append(maxMem).append(" MB")
743            .append("\n\n# Java 3D:")
744            .append("\n# Renderer: ").append(j3dProps.get("j3d.renderer"))
745            .append("\n# Pipeline: ").append(j3dProps.get("j3d.pipeline"))
746            .append("\n# Vendor:   ").append(j3dProps.get("native.vendor"))
747            .append("\n# Version:  ").append(j3dProps.get("j3d.version"))
748            .append("\n\n# Jython:")
749            .append("\n# Version:     ").append(jythonProps.get("sys.version_info"))
750            .append("\n# python.home: ").append(jythonProps.get("python.home"));
751            
752        if (firehose) {
753            buf.append("\n\n\n#Firehose:\n\n# SOFTWARE VERSIONS\n");
754            for (String key : new TreeSet<>(versions.keySet())) {
755                buf.append(key).append('=').append(versions.get(key)).append('\n');
756            }
757            
758            buf.append("\n# MACHINE PROPERTIES\n");
759            for (String key : new TreeSet<>(machineProps.keySet())) {
760                buf.append(key).append('=').append(machineProps.get(key)).append('\n');
761            }
762            
763            buf.append("\n# JAVA SYSTEM PROPERTIES\n");
764            for (Object key : new TreeSet<>(sysProps.keySet())) {
765                buf.append(key).append('=').append(sysProps.get(key)).append('\n');
766            }
767            
768            buf.append("\n# JAVA3D/JOGL PROPERTIES\n");
769            for (String key : new TreeSet<>(j3dProps.keySet())) {
770                buf.append(key).append('=').append(j3dProps.get(key)).append('\n');
771            }
772            
773            buf.append("\n# JYTHON PROPERTIES\n");
774            for (Object key : new TreeSet<>(jythonProps.keySet())) {
775                buf.append(key).append('=').append(jythonProps.get(key)).append('\n');
776            }
777            
778            // get idv/mcv properties
779            buf.append("\n# IDV AND MCIDAS-V PROPERTIES\n");
780            for (String key : new TreeSet<>(mcvProps.keySet())) {
781                buf.append(key).append('=').append(mcvProps.get(key)).append('\n');
782            }
783        }
784        return buf.toString();
785    }
786}