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