edu.wisc.ssec.mcidasv.util.SystemState.java Source code

Java tutorial

Introduction

Here is the source code for edu.wisc.ssec.mcidasv.util.SystemState.java

Source

/*
 * This file is part of McIDAS-V
 *
 * Copyright 2007-2019
 * Space Science and Engineering Center (SSEC)
 * University of Wisconsin - Madison
 * 1225 W. Dayton Street, Madison, WI 53706, USA
 * http://www.ssec.wisc.edu/mcidas
 * 
 * All Rights Reserved
 * 
 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
 * some McIDAS-V source code is based on IDV and VisAD source code.  
 * 
 * McIDAS-V is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 * 
 * McIDAS-V is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser Public License
 * along with this program.  If not, see http://www.gnu.org/licenses.
 */
package edu.wisc.ssec.mcidasv.util;

import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashMap;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.reflect.Method;

import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import java.util.TreeSet;

import java.awt.DisplayMode;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import javax.media.j3d.Canvas3D;
import javax.media.j3d.VirtualUniverse;

import com.google.common.base.Splitter;
import org.python.core.Py;
import org.python.core.PySystemState;

import org.python.modules.posix.PosixModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ucar.nc2.grib.GribConverterUtility;
import ucar.unidata.idv.ArgsManager;
import ucar.unidata.idv.IdvResourceManager.IdvResource;
import ucar.unidata.util.IOUtil;
import ucar.unidata.util.ResourceCollection;
import ucar.visad.display.DisplayUtil;

import edu.wisc.ssec.mcidasv.Constants;
import edu.wisc.ssec.mcidasv.McIDASV;
import edu.wisc.ssec.mcidasv.StateManager;

/**
 * Utility methods for querying the state of the user's machine.
 */
public class SystemState {

    /** Handy logging object. */
    private static final Logger logger = LoggerFactory.getLogger(SystemState.class);

    // Don't allow outside instantiation.
    private SystemState() {
    }

    public static String escapeWhitespaceChars(final CharSequence sequence) {
        StringBuilder sb = new StringBuilder(sequence.length() * 7);
        for (int i = 0; i < sequence.length(); i++) {
            switch (sequence.charAt(i)) {
            case '\t':
                sb.append("\\t");
                break;
            case '\n':
                sb.append('\\').append('n');
                break;
            case '\013':
                sb.append("\\013");
                break;
            case '\f':
                sb.append("\\f");
                break;
            case '\r':
                sb.append("\\r");
                break;
            case '\u0085':
                sb.append("\\u0085");
                break;
            case '\u1680':
                sb.append("\\u1680");
                break;
            case '\u2028':
                sb.append("\\u2028");
                break;
            case '\u2029':
                sb.append("\\u2029");
                break;
            case '\u205f':
                sb.append("\\u205f");
                break;
            case '\u3000':
                sb.append("\\u3000");
                break;
            }
        }
        return sb.toString();
    }

    /**
     * Attempt to invoke {@code OperatingSystemMXBean.methodName} via 
     * reflection.
     * 
     * @param <T> Either {@code Long} or {@code Double}.
     * @param methodName The method to invoke. Must belong to 
     * {@code com.sun.management.OperatingSystemMXBean}.
     * @param defaultValue Default value to return, must be in 
     * {@literal "boxed"} form.
     * 
     * @return Either the result of the {@code methodName} call or 
     * {@code defaultValue}.
     */
    private static <T> T hackyMethodCall(final String methodName, final T defaultValue) {
        assert methodName != null : "Cannot invoke a null method name";
        assert !methodName.isEmpty() : "Cannot invoke an empty method name";
        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
        T result = defaultValue;
        try {
            Method m = osBean.getClass().getMethod(methodName);
            m.setAccessible(true);
            // don't suppress warnings because we cannot guarantee that this
            // cast is correct.
            result = (T) m.invoke(osBean);
        } catch (Exception e) {
            logger.error("couldn't call method: " + methodName, e);
        }
        return result;
    }

    /**
     * Returns the contents of Jython's registry (basically just Jython-specific
     * properties) as well as some of the information from Python's 
     * {@literal "sys"} module. 
     * 
     * @return Jython's configuration settings. 
     */
    public static Map<Object, Object> queryJythonProps() {
        Map<Object, Object> properties = newLinkedHashMap(PySystemState.registry);
        try (PySystemState systemState = Py.getSystemState()) {
            properties.put("sys.argv", systemState.argv.toString());
            properties.put("sys.path", systemState.path);
            properties.put("sys.platform", systemState.platform.toString());
        }
        properties.put("sys.builtin_module_names", PySystemState.builtin_module_names.toString());
        properties.put("sys.byteorder", PySystemState.byteorder);
        properties.put("sys.isPackageCacheEnabled", PySystemState.isPackageCacheEnabled());
        properties.put("sys.version", PySystemState.version);
        properties.put("sys.version_info", PySystemState.version_info);
        return properties;
    }

    /**
     * Attempts to call methods belonging to 
     * {@code com.sun.management.OperatingSystemMXBean}. If successful, we'll
     * have the following information:
     * <ul>
     *   <li>opsys.memory.virtual.committed: virtual memory that is guaranteed to be available</li>
     *   <li>opsys.memory.swap.total: total amount of swap space in bytes</li>
     *   <li>opsys.memory.swap.free: free swap space in bytes</li>
     *   <li>opsys.cpu.time: CPU time used by the process (nanoseconds)</li>
     *   <li>opsys.memory.physical.free: free physical memory in bytes</li>
     *   <li>opsys.memory.physical.total: physical memory in bytes</li>
     *   <li>opsys.load: system load average for the last minute</li>
     * </ul>
     * 
     * @return Map of properties that contains interesting information about
     * the hardware McIDAS-V is using.
     */
    public static Map<String, String> queryOpSysProps() {
        Map<String, String> properties = newLinkedHashMap(10);
        long committed = hackyMethodCall("getCommittedVirtualMemorySize", Long.MIN_VALUE);
        long freeMemory = hackyMethodCall("getFreePhysicalMemorySize", Long.MIN_VALUE);
        long freeSwap = hackyMethodCall("getFreeSwapSpaceSize", Long.MIN_VALUE);
        long cpuTime = hackyMethodCall("getProcessCpuTime", Long.MIN_VALUE);
        long totalMemory = hackyMethodCall("getTotalPhysicalMemorySize", Long.MIN_VALUE);
        long totalSwap = hackyMethodCall("getTotalSwapSpaceSize", Long.MIN_VALUE);
        double loadAvg = hackyMethodCall("getSystemLoadAverage", Double.NaN);

        Runtime rt = Runtime.getRuntime();
        long currentMem = rt.totalMemory() - rt.freeMemory();

        properties.put("opsys.cpu.time", Long.toString(cpuTime));
        properties.put("opsys.load", Double.toString(loadAvg));
        properties.put("opsys.memory.jvm.current", Long.toString(currentMem));
        properties.put("opsys.memory.jvm.max", Long.toString(rt.maxMemory()));
        properties.put("opsys.memory.virtual.committed", Long.toString(committed));
        properties.put("opsys.memory.physical.free", Long.toString(freeMemory));
        properties.put("opsys.memory.physical.total", Long.toString(totalMemory));
        properties.put("opsys.memory.swap.free", Long.toString(freeSwap));
        properties.put("opsys.memory.swap.total", Long.toString(totalSwap));

        return properties;
    }

    /**
     * Polls Java for information about the user's machine. We're specifically
     * after memory statistics, number of processors, and display information.
     * 
     * @return {@link Map} of properties that describes the user's machine.
     */
    public static Map<String, String> queryMachine() {
        Map<String, String> props = newLinkedHashMap();

        // cpu count and whatnot
        int processors = Runtime.getRuntime().availableProcessors();
        props.put("opsys.cpu.count", Integer.toString(processors));

        // memory: available, used, etc
        props.putAll(queryOpSysProps());

        // screen: count, resolution(s)
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        int displayCount = ge.getScreenDevices().length;

        for (int i = 0; i < displayCount; i++) {
            String baseId = "opsys.display." + i + '.';
            GraphicsDevice dev = ge.getScreenDevices()[i];
            DisplayMode mode = dev.getDisplayMode();
            props.put(baseId + "name", dev.getIDstring());
            props.put(baseId + "depth", Integer.toString(mode.getBitDepth()));
            props.put(baseId + "width", Integer.toString(mode.getWidth()));
            props.put(baseId + "height", Integer.toString(mode.getHeight()));
            props.put(baseId + "refresh", Integer.toString(mode.getRefreshRate()));
        }
        return props;
    }

    /**
     * Returns a mapping of display number to a {@link java.awt.Rectangle} 
     * that represents the {@literal "bounds"} of the display.
     *
     * @return Rectangles representing the {@literal "bounds"} of the current
     * display devices.
     */
    public static Map<Integer, Rectangle> getDisplayBounds() {
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        int idx = 0;
        Map<Integer, Rectangle> map = newLinkedHashMap(ge.getScreenDevices().length * 2);
        for (GraphicsDevice dev : ge.getScreenDevices()) {
            for (GraphicsConfiguration config : dev.getConfigurations()) {
                map.put(idx++, config.getBounds());
            }
        }
        return map;
    }

    // TODO(jon): this should really be a polygon
    public static Rectangle getVirtualDisplayBounds() {
        Rectangle virtualBounds = new Rectangle();
        for (Rectangle bounds : getDisplayBounds().values()) {
            virtualBounds = virtualBounds.union(bounds);
        }
        return virtualBounds;
    }

    /**
     * Polls Java 3D for information about its environment. Specifically, we 
     * call {@link VirtualUniverse#getProperties()} and 
     * {@link Canvas3D#queryProperties()}.
     * 
     * @return As much information as Java 3D can provide.
     */
    @SuppressWarnings("unchecked") // casting to Object, so this should be fine.
    public static Map<String, Object> queryJava3d() {

        Map<String, Object> universeProps = (Map<String, Object>) VirtualUniverse.getProperties();

        GraphicsConfiguration config = DisplayUtil.getPreferredConfig(null, true, false);
        Map<String, Object> c3dMap = new Canvas3D(config).queryProperties();

        Map<String, Object> props = newLinkedHashMap(universeProps.size() + c3dMap.size());
        props.putAll(universeProps);
        props.putAll(c3dMap);
        return props;
    }

    /**
     * Gets a human-friendly representation of the information embedded within
     * IDV's {@code build.properties}.
     *
     * @return {@code String} that looks like {@literal "IDV version major.minor<b>revision</b> built <b>date</b>"}.
     * For example: {@code IDV version 2.9u4 built 2011-04-13 14:01 UTC}.
     */
    public static String getIdvVersionString() {
        Map<String, String> info = queryIdvBuildProperties();
        return "IDV version " + info.get("idv.version.major") + '.' + info.get("idv.version.minor")
                + info.get("idv.version.revision") + " built " + info.get("idv.build.date");
    }

    /**
     * Gets a human-friendly representation of the information embedded within
     * McIDAS-V's {@code build.properties}.
     * 
     * @return {@code String} that looks like {@literal "McIDAS-V version major.minor<b>release</b> built <b>date</b>"}.
     * For example: {@code McIDAS-V version 1.02beta1 built 2011-04-14 17:36}.
     */
    public static String getMcvVersionString() {
        Map<String, String> info = queryMcvBuildProperties();
        return "McIDAS-V version " + info.get(Constants.PROP_VERSION_MAJOR) + '.'
                + info.get(Constants.PROP_VERSION_MINOR) + info.get(Constants.PROP_VERSION_RELEASE) + " built "
                + info.get(Constants.PROP_BUILD_DATE);
    }

    /**
     * Gets a human-friendly representation of the version information embedded 
     * within VisAD's {@literal "DATE"} file.
     * 
     * @return {@code String} that looks
     * {@literal "VisAD version <b>revision</b> built <b>date</b>"}.
     * For example: {@code VisAD version 5952 built Thu Mar 22 13:01:31 CDT 2012}.
     */
    public static String getVisadVersionString() {
        Map<String, String> props = queryVisadBuildProperties();
        return "VisAD version " + props.get(Constants.PROP_VISAD_REVISION) + " built "
                + props.get(Constants.PROP_VISAD_DATE);
    }

    /**
     * Gets a human-friendly representation of the ncIdv.jar version
     * information.
     *
     * @return {@code String} that looks like
     * {@literal "netCDF-Java version <b>revision</b> <b>built</b>"}.
     */
    public static String getNcidvVersionString() {
        Map<String, String> props = queryNcidvBuildProperties();
        return "netCDF-Java version " + props.get("version") + " built " + props.get("buildDate");
    }

    /**
     * Open a file for reading.
     *
     * @param name File to open.
     *
     * @return {@code InputStream} used to read {@code name}, or {@code null}
     * if {@code name} could not be found.
     */
    private static InputStream resourceAsStream(final String name) {
        return ClassLoader.getSystemResourceAsStream(name);
    }

    /**
     * Returns a {@link Map} containing any relevant version information. 
     * 
     * <p>Currently this information consists of the date visad.jar was built, 
     * as well as the (then-current) Subversion revision number.
     * 
     * @return {@code Map} of the contents of VisAD's DATE file.
     */
    public static Map<String, String> queryVisadBuildProperties() {
        Map<String, String> props = newLinkedHashMap(4);

        try (BufferedReader input = new BufferedReader(new InputStreamReader(resourceAsStream("DATE")))) {
            String contents = input.readLine();
            // string should look like: Thu Mar 22 13:01:31 CDT 2012  Rev:5952
            String splitAt = "  Rev:";
            int index = contents.indexOf(splitAt);
            String buildDate = "ERROR";
            String revision = "ERROR";
            String parseFail = "true";
            if (index > 0) {
                buildDate = contents.substring(0, index);
                revision = contents.substring(index + splitAt.length());
                parseFail = "false";
            }
            props.put(Constants.PROP_VISAD_ORIGINAL, contents);
            props.put(Constants.PROP_VISAD_PARSE_FAIL, parseFail);
            props.put(Constants.PROP_VISAD_DATE, buildDate);
            props.put(Constants.PROP_VISAD_REVISION, revision);
        } catch (IOException e) {
            logger.error("could not read from VisAD DATE file", e);
        }
        return props;
    }

    /**
     * Determine the actual name of the McIDAS-V JAR file.
     *
     * <p>This is needed because we're now following the Maven naming
     * convention, and this means that {@literal "mcidasv.jar"} no longer
     * exists.</p>
     *
     * @param jarDir Directory containing all of the McIDAS-V JARs.
     *               Cannot be {@code null}.
     *
     * @return Name (note: not path) of the McIDAS-V JAR file.
     *
     * @throws IOException if there was a problem locating the McIDAS-V JAR.
     */
    private static String getMcvJarname(String jarDir) throws IOException {
        File dir = new File(jarDir);
        File[] files = dir.listFiles();
        if (files == null) {
            throw new IOException("Could not get list of files within " + "'" + jarDir + "'");
        }
        for (File f : files) {
            String name = f.getName();
            if (name.startsWith("mcidasv-") && name.endsWith(".jar")) {
                return name;
            }
        }
        throw new FileNotFoundException("Could not find McIDAS-V JAR file");
    }

    /**
     * Return McIDAS-V's classpath.
     *
     * <p>This may differ from what is reported by 
     * {@code System.getProperty("java.class.path")}. This is because McIDAS-V
     * specifies its classpath using {@code META-INF/MANIFEST.MF} in 
     * {@code mcidasv.jar}.
     * </p>
     *
     * <p>This is used by console_init.py to figure out where the VisAD,
     * IDV, and McIDAS-V JAR files are located.</p>
     *
     * @return Either a list of strings containing the path to each JAR file
     * in the classpath, or an empty list.
     */
    public static List<String> getMcvJarClasspath() {
        String jarDir = System.getProperty("user.dir");
        if (jarDir == null) {
            jarDir = PosixModule.getcwd().toString();
        }

        // 64 chosen because we're currently at 38 JARs.
        List<String> jars = arrList(64);
        try {
            String mcvJar = getMcvJarname(jarDir);
            Path p = Paths.get(jarDir, mcvJar);
            String path = "jar:file:" + p.toString() + "!/META-INF/MANIFEST.MF";
            InputStream stream = IOUtil.getInputStream(path);
            if (stream != null) {
                Manifest manifest = new Manifest(stream);
                Attributes attrs = manifest.getMainAttributes();
                if (attrs != null) {
                    String classpath = attrs.getValue("Class-Path");
                    Splitter split = Splitter.on(' ').trimResults().omitEmptyStrings();
                    for (String jar : split.split(classpath)) {
                        jars.add(Paths.get(jarDir, jar).toFile().toString());
                    }
                }
            }
        } catch (IOException | NullPointerException e) {
            logger.warn("Exception occurred:", e);
        }
        return jars;
    }

    /**
     * Returns a {@link Map} containing {@code version} and {@code buildDate}
     * keys.
     *
     * @return {@code Map} containing netCDF-Java version and build date.
     */
    public static Map<String, String> queryNcidvBuildProperties() {
        // largely taken from LibVersionUtil's getBuildInfo method.
        GribConverterUtility util = new GribConverterUtility();
        Map<String, String> buildInfo = new HashMap<>(4);
        // format from ncidv.jar
        SimpleDateFormat formatIn = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
        // format of output timestamp
        SimpleDateFormat formatOut = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ");
        formatOut.setTimeZone(TimeZone.getTimeZone("UTC"));
        try {
            Enumeration<URL> resources = util.getClass().getClassLoader().getResources("META-INF/MANIFEST.MF");

            while (resources.hasMoreElements()) {
                Manifest manifest = new Manifest(resources.nextElement().openStream());
                Attributes attrs = manifest.getMainAttributes();
                if (attrs != null) {
                    String implTitle = attrs.getValue("Implementation-Title");
                    if ((implTitle != null) && implTitle.contains("ncIdv")) {
                        buildInfo.put("version", attrs.getValue("Implementation-Version"));
                        String strDate = attrs.getValue("Built-On");
                        Date date = formatIn.parse(strDate);
                        buildInfo.put("buildDate", formatOut.format(date));
                        break;
                    }
                }
            }
        } catch (IOException e) {
            logger.warn("exception occurred:", e);
        } catch (ParseException pe) {
            logger.warn("failed to parse build date from ncIdv.jar", pe);
        }
        return buildInfo;
    }

    /**
     * Returns a {@link Map} of the (currently) most useful contents of
     * {@code ucar/unidata/idv/resources/build.properties}.
     *
     * <p>Consider the output of {@link #getIdvVersionString()}; it's built
     * with the the following:
     * <ul>
     *   <li><b>{@code idv.version.major}</b>: currently {@literal "3"}</li>
     *   <li><b>{@code idv.version.minor}</b>: currently {@literal "0"}</li>
     *   <li><b>{@code idv.version.revision}</b>: currently {@literal "u2"}}</li>
     *   <li><b>{@code idv.build.date}</b>: varies pretty frequently,
     *   as it's the build timestamp for idv.jar</li>
     * </ul>
     *
     * @return A {@code Map} of at least the useful parts of build.properties.
     */
    public static Map<String, String> queryIdvBuildProperties() {
        Map<String, String> versions = newLinkedHashMap(4);
        InputStream input = null;
        try {
            input = resourceAsStream("ucar/unidata/idv/resources/build.properties");
            Properties props = new Properties();
            props.load(input);
            String major = props.getProperty("idv.version.major", "no_major");
            String minor = props.getProperty("idv.version.minor", "no_minor");
            String revision = props.getProperty("idv.version.revision", "no_revision");
            String date = props.getProperty("idv.build.date", "");
            versions.put("idv.version.major", major);
            versions.put("idv.version.minor", minor);
            versions.put("idv.version.revision", revision);
            versions.put("idv.build.date", date);
        } catch (Exception e) {
            logger.error("could not read from IDV build.properties", e);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (Exception ex) {
                    logger.error("could not close IDV build.properties", ex);
                }
            }
        }
        return versions;
    }

    /**
     * Returns a {@link Map} of the (currently) most useful contents of
     * {@code edu/wisc/ssec/mcidasv/resources/build.properties}.
     *
     * <p>Consider the output of {@link #getMcvVersionString()}; it's built
     * with the the following:
     * <ul>
     *   <li><b>{@code mcidasv.version.major}</b>:
     *   currently {@literal "1"}</li>
     *   <li><b>{@code mcidasv.version.minor}</b>:
     *   currently {@literal "02"}</li>
     *   <li><b>{@code mcidasv.version.release}</b>: currently
     *   {@literal "beta1"}</li>
     *   <li><b>{@code mcidasv.build.date}</b>: varies pretty frequently, as
     *   it's the build timestamp for mcidasv.jar.</li>
     * </ul>
     *
     * @return A {@code Map} of at least the useful parts of build.properties.
     */
    public static Map<String, String> queryMcvBuildProperties() {
        Map<String, String> versions = newLinkedHashMap(4);
        InputStream input = null;
        try {
            input = resourceAsStream("edu/wisc/ssec/mcidasv/resources/build.properties");
            Properties props = new Properties();
            props.load(input);
            String major = props.getProperty(Constants.PROP_VERSION_MAJOR, "0");
            String minor = props.getProperty(Constants.PROP_VERSION_MINOR, "0");
            String release = props.getProperty(Constants.PROP_VERSION_RELEASE, "");
            String date = props.getProperty(Constants.PROP_BUILD_DATE, "Unknown");
            versions.put(Constants.PROP_VERSION_MAJOR, major);
            versions.put(Constants.PROP_VERSION_MINOR, minor);
            versions.put(Constants.PROP_VERSION_RELEASE, release);
            versions.put(Constants.PROP_BUILD_DATE, date);
        } catch (Exception e) {
            logger.error("could not read from McIDAS-V build.properties!", e);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (Exception ex) {
                    logger.error("could not close McIDAS-V build.properties!", ex);
                }
            }
        }
        return versions;
    }

    /**
     * Queries McIDAS-V for information about its state. There's not a good way
     * to characterize what we're interested in, so let's leave it at 
     * {@literal "whatever seems useful"}.
     * 
     * @param mcv The McIDASV {@literal "god"} object.
     * 
     * @return Information about the state of McIDAS-V.
     */
    // need: argsmanager, resource manager
    public static Map<String, Object> queryMcvState(final McIDASV mcv) {
        // through some simple verification, props generally has under 250 elements
        Map<String, Object> props = newLinkedHashMap(250);

        ArgsManager args = mcv.getArgsManager();
        props.put("mcv.state.islinteractive", args.getIslInteractive());
        props.put("mcv.state.offscreen", args.getIsOffScreen());
        props.put("mcv.state.initcatalogs", args.getInitCatalogs());
        props.put("mcv.state.actions", mcv.getActionHistory());
        props.put("mcv.plugins.installed", args.installPlugins);
        props.put("mcv.state.commandline", mcv.getCommandLineArgs());

        // loop through resources
        List<IdvResource> resources = (List<IdvResource>) mcv.getResourceManager().getResources();
        for (IdvResource resource : resources) {
            String id = resource.getId();
            props.put(id + ".description", resource.getDescription());
            if (resource.getPattern() == null) {
                props.put(id + ".pattern", "null");
            } else {
                props.put(id + ".pattern", resource.getPattern());
            }

            ResourceCollection rc = mcv.getResourceManager().getResources(resource);
            int rcSize = rc.size();
            List<String> specified = arrList(rcSize);
            List<String> valid = arrList(rcSize);
            for (int i = 0; i < rcSize; i++) {
                String tmpResource = (String) rc.get(i);
                specified.add(tmpResource);
                if (rc.isValid(i)) {
                    valid.add(tmpResource);
                }
            }

            props.put(id + ".specified", specified);
            props.put(id + ".existing", valid);
        }
        return props;
    }

    /**
     * Builds a (filtered) subset of the McIDAS-V system properties and returns
     * the results as a {@code String}.
     * 
     * @param mcv The McIDASV {@literal "god"} object.
     * 
     * @return The McIDAS-V system properties in the following format: 
     * {@code KEY=VALUE\n}. This is so we kinda-sorta conform to the standard
     * {@link Properties} file format.
     * 
     * @see #getStateAsString(edu.wisc.ssec.mcidasv.McIDASV, boolean)
     */
    public static String getStateAsString(final McIDASV mcv) {
        return getStateAsString(mcv, false);
    }

    /**
     * Builds the McIDAS-V system properties and returns the results as a 
     * {@code String}.
     * 
     * @param mcv The McIDASV {@literal "god"} object.
     * @param firehose If {@code true}, enables {@literal "unfiltered"} output.
     * 
     * @return The McIDAS-V system properties in the following format: 
     * {@code KEY=VALUE\n}. This is so we kinda-sorta conform to the standard
     * {@link Properties} file format.
     */
    public static String getStateAsString(final McIDASV mcv, final boolean firehose) {
        int builderSize = firehose ? 45000 : 1000;
        StringBuilder buf = new StringBuilder(builderSize);

        Map<String, String> versions = ((StateManager) mcv.getStateManager()).getVersionInfo();
        Properties sysProps = System.getProperties();
        Map<String, Object> j3dProps = queryJava3d();
        Map<String, String> machineProps = queryMachine();
        Map<Object, Object> jythonProps = queryJythonProps();
        Map<String, Object> mcvProps = queryMcvState(mcv);

        if (sysProps.contains("line.separator")) {
            sysProps.put("line.separator", escapeWhitespaceChars((String) sysProps.get("line.separator")));
        }

        String maxMem = Long.toString(Long.valueOf(machineProps.get("opsys.memory.jvm.max")) / 1048576L);
        String curMem = Long.toString(Long.valueOf(machineProps.get("opsys.memory.jvm.current")) / 1048576L);

        buf.append("# Software Versions:").append("\n# McIDAS-V:    ").append(versions.get("mcv.version.general"))
                .append(" (").append(versions.get("mcv.version.build")).append(')').append("\n# VisAD:       ")
                .append(versions.get("visad.version.general")).append(" (")
                .append(versions.get("visad.version.build")).append(')').append("\n# IDV:         ")
                .append(versions.get("idv.version.general")).append(" (").append(versions.get("idv.version.build"))
                .append(')').append("\n# netcdf-Java: ").append(versions.get("netcdf.version.general")).append(" (")
                .append(versions.get("netcdf.version.build")).append(')').append("\n\n# Operating System:")
                .append("\n# Name:         ").append(sysProps.getProperty("os.name")).append("\n# Version:      ")
                .append(sysProps.getProperty("os.version")).append("\n# Architecture: ")
                .append(sysProps.getProperty("os.arch")).append("\n\n# Java:").append("\n# Version: ")
                .append(sysProps.getProperty("java.version")).append("\n# Vendor:  ")
                .append(sysProps.getProperty("java.vendor")).append("\n# Home:    ")
                .append(sysProps.getProperty("java.home")).append("\n\n# JVM Memory").append("\n# Current: ")
                .append(curMem).append(" MB").append("\n# Maximum: ").append(maxMem).append(" MB")
                .append("\n\n# Java 3D:").append("\n# Renderer: ").append(j3dProps.get("j3d.renderer"))
                .append("\n# Pipeline: ").append(j3dProps.get("j3d.pipeline")).append("\n# Vendor:   ")
                .append(j3dProps.get("native.vendor")).append("\n# Version:  ").append(j3dProps.get("j3d.version"))
                .append("\n\n# Jython:").append("\n# Version:     ").append(jythonProps.get("sys.version_info"))
                .append("\n# python.home: ").append(jythonProps.get("python.home"));

        if (firehose) {
            buf.append("\n\n\n#Firehose:\n\n# SOFTWARE VERSIONS\n");
            for (String key : new TreeSet<>(versions.keySet())) {
                buf.append(key).append('=').append(versions.get(key)).append('\n');
            }

            buf.append("\n# MACHINE PROPERTIES\n");
            for (String key : new TreeSet<>(machineProps.keySet())) {
                buf.append(key).append('=').append(machineProps.get(key)).append('\n');
            }

            buf.append("\n# JAVA SYSTEM PROPERTIES\n");
            for (Object key : new TreeSet<>(sysProps.keySet())) {
                buf.append(key).append('=').append(sysProps.get(key)).append('\n');
            }

            buf.append("\n# JAVA3D/JOGL PROPERTIES\n");
            for (String key : new TreeSet<>(j3dProps.keySet())) {
                buf.append(key).append('=').append(j3dProps.get(key)).append('\n');
            }

            buf.append("\n# JYTHON PROPERTIES\n");
            for (Object key : new TreeSet<>(jythonProps.keySet())) {
                buf.append(key).append('=').append(jythonProps.get(key)).append('\n');
            }

            // get idv/mcv properties
            buf.append("\n# IDV AND MCIDAS-V PROPERTIES\n");
            for (String key : new TreeSet<>(mcvProps.keySet())) {
                buf.append(key).append('=').append(mcvProps.get(key)).append('\n');
            }
        }
        return buf.toString();
    }
}