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