001/*
002 * $Id: SystemState.java,v 1.9 2011/04/14 17:45:50 jbeavers Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030package edu.wisc.ssec.mcidasv.util;
031
032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
033import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast;
034import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashMap;
035
036//import static edu.wisc.ssec.mcidasv.Constants.PROP_VERSION_MAJOR;
037//import static edu.wisc.ssec.mcidasv.Constants.PROP_VERSION_MINOR;
038//import static edu.wisc.ssec.mcidasv.Constants.PROP_VERSION_RELEASE;
039//import static edu.wisc.ssec.mcidasv.Constants.PROP_BUILD_DATE;
040
041import java.io.InputStream;
042import java.lang.management.ManagementFactory;
043import java.lang.management.OperatingSystemMXBean;
044import java.lang.reflect.Method;
045
046import java.util.List;
047import java.util.Map;
048import java.util.Properties;
049import java.util.TreeSet;
050
051import java.awt.DisplayMode;
052import java.awt.GraphicsConfiguration;
053import java.awt.GraphicsDevice;
054import java.awt.GraphicsEnvironment;
055import java.awt.Rectangle;
056
057import javax.media.j3d.Canvas3D;
058import javax.media.j3d.VirtualUniverse;
059
060import org.python.core.Py;
061import org.python.core.PySystemState;
062
063import org.slf4j.Logger;
064import org.slf4j.LoggerFactory;
065
066import ucar.unidata.idv.ArgsManager;
067import ucar.unidata.idv.IdvResourceManager.IdvResource;
068import ucar.unidata.util.ResourceCollection;
069import ucar.visad.display.DisplayUtil;
070
071import edu.wisc.ssec.mcidasv.Constants;
072import edu.wisc.ssec.mcidasv.McIDASV;
073import edu.wisc.ssec.mcidasv.StateManager;
074
075/**
076 * Utility methods for querying the state of the user's machine.
077 */
078public class SystemState {
079
080    /** Handy logging object. */
081    private static final Logger logger = LoggerFactory.getLogger(SystemState.class);
082
083    // Don't allow outside instantiation.
084    private SystemState() { }
085
086    /**
087     * Attempt to invoke {@code OperatingSystemMXBean.methodName} via 
088     * reflection.
089     * 
090     * @param <T> Either {@code Long} or {@code Double}.
091     * @param methodName The method to invoke. Must belong to 
092     * {@code com.sun.management.OperatingSystemMXBean}.
093     * @param defaultValue Default value to return, must be in 
094     * {@literal "boxed"} form.
095     * 
096     * @return Either the result of the {@code methodName} call or 
097     * {@code defaultValue}.
098     */
099    private static <T> T hackyMethodCall(final String methodName, final T defaultValue) {
100        assert methodName != null : "Cannot invoke a null method name";
101        assert methodName.length() > 0: "Cannot invoke an empty method name";
102        OperatingSystemMXBean osBean = 
103            ManagementFactory.getOperatingSystemMXBean();
104        T result = defaultValue;
105        try {
106            Method m = osBean.getClass().getMethod(methodName);
107            m.setAccessible(true);
108            // don't suppress warnings because we cannot guarantee that this
109            // cast is correct.
110            result = (T)m.invoke(osBean);
111        } catch (Exception e) {
112            logger.error("couldn't call method: " + methodName, e);
113        }
114        return result;
115    }
116
117    /**
118     * Returns the contents of Jython's registry (basically just Jython-specific
119     * properties) as well as some of the information from Python's 
120     * {@literal "sys"} module. 
121     * 
122     * @return Jython's configuration settings. 
123     */
124    public static Map<Object, Object> queryJythonProps() {
125        Map<Object, Object> properties = newLinkedHashMap(PySystemState.registry);
126        properties.put("sys.argv", Py.getSystemState().argv.toString());
127        properties.put("sys.builtin_module_names", PySystemState.builtin_module_names.toString());
128        properties.put("sys.byteorder", PySystemState.byteorder);
129        properties.put("sys.isPackageCacheEnabled", PySystemState.isPackageCacheEnabled());
130        properties.put("sys.path", Py.getSystemState().path);
131        properties.put("sys.platform", PySystemState.platform.toString());
132        properties.put("sys.version", PySystemState.version);
133        properties.put("sys.version_info", PySystemState.version_info);
134        return properties;
135    }
136
137    /**
138     * Attempts to call methods belonging to 
139     * {@code com.sun.management.OperatingSystemMXBean}. If successful, we'll
140     * have the following information:
141     * <ul>
142     *   <li>opsys.memory.virtual.committed: virtual memory that is guaranteed to be available</li>
143     *   <li>opsys.memory.swap.total: total amount of swap space in bytes</li>
144     *   <li>opsys.memory.swap.free: free swap space in bytes</li>
145     *   <li>opsys.cpu.time: CPU time used by the process (nanoseconds)</li>
146     *   <li>opsys.memory.physical.free: free physical memory in bytes</li>
147     *   <li>opsys.memory.physical.total: physical memory in bytes</li>
148     *   <li>opsys.load: system load average for the last minute</li>
149     * </ul>
150     * 
151     * @return Map of properties that contains interesting information about
152     * the hardware McIDAS-V is using.
153     */
154    public static Map<String, String> queryOpSysProps() {
155        Map<String, String> properties = newLinkedHashMap(10);
156        long committed = hackyMethodCall("getCommittedVirtualMemorySize", Long.MIN_VALUE);
157        long freeMemory = hackyMethodCall("getFreePhysicalMemorySize", Long.MIN_VALUE);
158        long freeSwap = hackyMethodCall("getFreeSwapSpaceSize", Long.MIN_VALUE);
159        long cpuTime = hackyMethodCall("getProcessCpuTime", Long.MIN_VALUE);
160        long totalMemory = hackyMethodCall("getTotalPhysicalMemorySize", Long.MIN_VALUE);
161        long totalSwap = hackyMethodCall("getTotalSwapSpaceSize", Long.MIN_VALUE);
162        double loadAvg = hackyMethodCall("getSystemLoadAverage", Double.NaN);
163
164        properties.put("opsys.cpu.time", Long.toString(cpuTime));
165        properties.put("opsys.load", Double.toString(loadAvg));
166        properties.put("opsys.memory.virtual.committed", Long.toString(committed));
167        properties.put("opsys.memory.physical.free", Long.toString(freeMemory));
168        properties.put("opsys.memory.physical.total", Long.toString(totalMemory));
169        properties.put("opsys.memory.swap.free", Long.toString(freeSwap));
170        properties.put("opsys.memory.swap.total", Long.toString(totalSwap));
171
172        return properties;
173    }
174
175    /**
176     * Polls Java for information about the user's machine. We're specifically
177     * after memory statistics, number of processors, and display information.
178     * 
179     * @return {@link Map} of properties that describes the user's machine.
180     */
181    public static Map<String, String> queryMachine() {
182        Map<String, String> props = newLinkedHashMap();
183
184        // cpu count and whatnot
185        int processors = Runtime.getRuntime().availableProcessors();
186        props.put("opsys.cpu.count", Integer.toString(processors));
187
188        // memory: available, used, etc
189        props.putAll(queryOpSysProps());
190
191        // screen: count, resolution(s)
192        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
193        int displayCount = ge.getScreenDevices().length;
194
195        for (int i = 0; i < displayCount; i++) {
196            String baseId = "opsys.display."+i+'.';
197            GraphicsDevice dev = ge.getScreenDevices()[i];
198            DisplayMode mode = dev.getDisplayMode();
199            props.put(baseId+"name", dev.getIDstring());
200            props.put(baseId+"depth", Integer.toString(mode.getBitDepth()));
201            props.put(baseId+"width", Integer.toString(mode.getWidth()));
202            props.put(baseId+"height", Integer.toString(mode.getHeight()));
203            props.put(baseId+"refresh", Integer.toString(mode.getRefreshRate()));
204        }
205        return props;
206    }
207
208    /**
209     * Returns a mapping of display number to a {@link java.awt.Rectangle} 
210     * that represents the {@literal "bounds"} of the display.
211     */
212    public static Map<Integer, Rectangle> getDisplayBounds() {
213        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
214        int idx = 0;
215        Map<Integer, Rectangle> map = newLinkedHashMap(ge.getScreenDevices().length * 2);
216        for (GraphicsDevice dev : ge.getScreenDevices()) {
217            for (GraphicsConfiguration config : dev.getConfigurations()) {
218                map.put(idx++, config.getBounds());
219            }
220        }
221        return map;
222    }
223
224    // TODO(jon): this should really be a polygon
225    public static Rectangle getVirtualDisplayBounds() {
226        Rectangle virtualBounds = new Rectangle();
227        for (Rectangle bounds : getDisplayBounds().values()) {
228            virtualBounds = virtualBounds.union(bounds);
229        }
230        return virtualBounds;
231    }
232
233    /**
234     * Polls Java 3D for information about its environment. Specifically, we 
235     * call {@link VirtualUniverse#getProperties()} and 
236     * {@link Canvas3D#queryProperties()}.
237     * 
238     * @return As much information as Java 3D can provide.
239     */
240    @SuppressWarnings("unchecked") // casting to Object, so this should be fine.
241    public static Map<String, Object> queryJava3d() {
242
243        Map<String, Object> universeProps = 
244            (Map<String, Object>)VirtualUniverse.getProperties();
245
246        GraphicsConfiguration config =
247            DisplayUtil.getPreferredConfig(null, true, false);
248        Map<String, Object> c3dMap = new Canvas3D(config).queryProperties();
249
250        Map<String, Object> props =
251                newLinkedHashMap(universeProps.size() + c3dMap.size());
252        props.putAll(universeProps);
253        props.putAll(c3dMap);
254        return props;
255    }
256
257    /**
258     * Gets a human-friendly representation of the information embedded within
259     * IDV's {@code build.properties}.
260     *
261     * @return {@code String} that looks like {@literal "IDV version major.minor<b>revision</b> built <b>date</b>"}.
262     * For example: {@code IDV version 2.9u4 built 2011-04-13 14:01 UTC}.
263     */
264    public static String getIdvVersionString() {
265        Map<String, String> info = queryIdvBuildProperties();
266        return "IDV version " + info.get("idv.version.major") + '.' +
267               info.get("idv.version.minor") + info.get("idv.version.revision") +
268               " built " + info.get("idv.build.date");
269    }
270
271    /**
272     * Gets a human-friendly representation of the information embedded within
273     * McIDAS-V's {@code build.properties}.
274     * 
275     * @return {@code String} that looks like {@literal "McIDAS-V version major.minor<b>release</b> built <b>date</b>"}.
276     * For example: {@code McIDAS-V version 1.02beta1 built 2011-04-14 17:36}.
277     */
278    public static String getMcvVersionString() {
279        Map<String, String> info = queryMcvBuildProperties();
280        return "McIDAS-V version " + info.get(Constants.PROP_VERSION_MAJOR) + '.' +
281               info.get(Constants.PROP_VERSION_MINOR) + info.get(Constants.PROP_VERSION_RELEASE) +
282               " built " + info.get(Constants.PROP_BUILD_DATE);
283    }
284
285    private InputStream getResourceAsStream(final String name) {
286        return ClassLoader.getSystemResourceAsStream(name);
287    }
288
289    public static Map<String, String> queryIdvBuildProperties() {
290        SystemState sysState = new SystemState();
291        Map<String, String> versions = newLinkedHashMap(4);
292        InputStream input = null;
293        try {
294            input = sysState.getResourceAsStream("ucar/unidata/idv/resources/build.properties");
295            Properties props = new Properties();
296            props.load(input);
297            String major = props.getProperty("idv.version.major", "no_major");
298            String minor = props.getProperty("idv.version.minor", "no_minor");
299            String revision = props.getProperty("idv.version.revision", "no_revision");
300            String date = props.getProperty("idv.build.date", "");
301            versions.put("idv.version.major", major);
302            versions.put("idv.version.minor", minor);
303            versions.put("idv.version.revision", revision);
304            versions.put("idv.build.date", date);
305        } catch (Exception e) {
306            logger.error("could not read from idv's build.properties", e);
307        } finally {
308            sysState = null;
309            if (input != null) {
310                try {
311                    input.close();
312                } catch (Exception ex) {
313                    logger.error("could not close idv's build.properties", ex);
314                }
315            }
316        }
317        return versions;
318    }
319
320    public static Map<String, String> queryMcvBuildProperties() {
321        SystemState sysState = new SystemState();
322        Map<String, String> versions = newLinkedHashMap(4);
323        InputStream input = null;
324        try {
325            input = sysState.getResourceAsStream("edu/wisc/ssec/mcidasv/resources/build.properties");
326            Properties props = new Properties();
327            props.load(input);
328            String major = props.getProperty(Constants.PROP_VERSION_MAJOR, "0");
329            String minor = props.getProperty(Constants.PROP_VERSION_MINOR, "0");
330            String release = props.getProperty(Constants.PROP_VERSION_RELEASE, "");
331            String date = props.getProperty(Constants.PROP_BUILD_DATE, "Unknown");
332            versions.put(Constants.PROP_VERSION_MAJOR, major);
333            versions.put(Constants.PROP_VERSION_MINOR, minor);
334            versions.put(Constants.PROP_VERSION_RELEASE, release);
335            versions.put(Constants.PROP_BUILD_DATE, date);
336        } catch (Exception e) {
337            logger.error("could not read from mcv's build.properties!", e);
338        } finally {
339            sysState = null;
340            if (input != null) {
341                try {
342                    input.close();
343                } catch (Exception ex) {
344                    logger.error("could not close mcv's build.properties!", ex);
345                }
346            }
347        }
348        return versions;
349    }
350
351    /**
352     * Queries McIDAS-V for information about its state. There's not a good way
353     * to characterize what we're interested in, so let's leave it at 
354     * {@literal "whatever seems useful"}.
355     * 
356     * @return Information about the state of McIDAS-V.
357     */
358    // need: argsmanager, resource manager
359    public static Map<String, Object> queryMcvState(final McIDASV mcv) {
360        Map<String, Object> props = newLinkedHashMap();
361
362        ArgsManager args = mcv.getArgsManager();
363        props.put("mcv.state.islinteractive", args.getIslInteractive());
364        props.put("mcv.state.offscreen", args.getIsOffScreen());
365        props.put("mcv.state.initcatalogs", args.getInitCatalogs());
366        props.put("mcv.state.actions", mcv.getActionHistory());
367        props.put("mcv.plugins.installed", args.installPlugins);
368        props.put("mcv.state.commandline", mcv.getCommandLineArgs());
369
370        // loop through resources
371        List<IdvResource> resources =
372                cast(mcv.getResourceManager().getResources());
373        for (IdvResource resource : resources) {
374            String id = resource.getId();
375            props.put(id+".description", resource.getDescription());
376            if (resource.getPattern() == null) {
377                props.put(id+".pattern", "null");
378            } else {
379                props.put(id+".pattern", resource.getPattern());
380            }
381
382            ResourceCollection rc = mcv.getResourceManager().getResources(resource);
383            int rcSize = rc.size();
384            List<String> specified = arrList(rcSize);
385            List<String> valid = arrList(rcSize);
386            for (int i = 0; i < rcSize; i++) {
387                String tmpResource = (String)rc.get(i);
388                specified.add(tmpResource);
389                if (rc.isValid(i)) {
390                    valid.add(tmpResource);
391                }
392            }
393
394            props.put(id+".specified", specified);
395            props.put(id+".existing", valid);
396        }
397        return props;
398    }
399
400    /**
401     * Builds a (filtered) subset of the McIDAS-V system properties and returns
402     * the results as a {@code String}.
403     * 
404     * @return The McIDAS-V system properties in the following format: 
405     * {@code KEY=VALUE\n}. This is so we kinda-sorta conform to the standard
406     * {@link Properties} file format.
407     * 
408     * @see #getStateAsString(edu.wisc.ssec.mcidasv.McIDASV, boolean)
409     */
410    public static String getStateAsString(final McIDASV mcv) {
411        return getStateAsString(mcv, false);
412    }
413
414    /**
415     * Builds the McIDAS-V system properties and returns the results as a 
416     * {@code String}.
417     * 
418     * @param firehose If {@code true}, enables {@literal "unfiltered"} output.
419     * 
420     * @return The McIDAS-V system properties in the following format: 
421     * {@code KEY=VALUE\n}. This is so we kinda-sorta conform to the standard
422     * {@link Properties} file format.
423     */
424    public static String getStateAsString(final McIDASV mcv, final boolean firehose) {
425        StringBuilder buf = new StringBuilder(20000);
426
427        Map<String, String> versions = ((StateManager)mcv.getStateManager()).getVersionInfo();
428        Properties sysProps = System.getProperties();
429        Map<String, Object> j3dProps = queryJava3d();
430        Map<String, String> machineProps = queryMachine();
431        Map<Object, Object> jythonProps = queryJythonProps();
432        Map<String, Object> mcvProps = queryMcvState(mcv);
433
434        buf.append("Software Versions:")
435            .append("\nMcIDAS-V: ").append(versions.get("mcv.version.general")).append(" (").append(versions.get("mcv.version.build")).append(')')
436            .append("\nIDV:      ").append(versions.get("idv.version.general")).append(" (").append(versions.get("idv.version.build")).append(')')
437            .append("\n\nOperating System:")
438            .append("\nName:         ").append(sysProps.getProperty("os.name"))
439            .append("\nVersion:      ").append(sysProps.getProperty("os.version"))
440            .append("\nArchitecture: ").append(sysProps.getProperty("os.arch"))
441            .append("\n\nJava:")
442            .append("\nVersion: ").append(sysProps.getProperty("java.version"))
443            .append("\nVendor:  ").append(sysProps.getProperty("java.vendor"))
444            .append("\nHome:    ").append(sysProps.getProperty("java.home"))
445            .append("\n\nJava 3D:")
446            .append("\nRenderer: ").append(j3dProps.get("j3d.renderer"))
447            .append("\nPipeline: ").append(j3dProps.get("j3d.pipeline"))
448            .append("\nVendor:   ").append(j3dProps.get("j3d.vendor"))
449            .append("\nVersion:  ").append(j3dProps.get("j3d.version"))
450            .append("\n\nJython:")
451            .append("\nVersion:     ").append(jythonProps.get("sys.version_info"))
452            .append("\npython.home: ").append(jythonProps.get("python.home"));
453
454        if (firehose) {
455            buf.append("\n\n\nFirehose:\n\n# SOFTWARE VERSIONS\n");
456            for (String key : (new TreeSet<String>(versions.keySet()))) {
457                buf.append(key).append('=').append(versions.get(key)).append('\n');
458            }
459
460            buf.append("\n# MACHINE PROPERTIES\n");
461            for (String key : (new TreeSet<String>(machineProps.keySet()))) {
462                buf.append(key).append('=').append(machineProps.get(key)).append('\n');
463            }
464
465            buf.append("\n# JAVA SYSTEM PROPERTIES\n");
466            for (Object key : (new TreeSet<Object>(sysProps.keySet()))) {
467                buf.append(key).append('=').append(sysProps.get(key)).append('\n');
468            }
469
470            buf.append("\n# JAVA3D/JOGL PROPERTIES\n");
471            for (String key : (new TreeSet<String>(j3dProps.keySet()))) {
472                buf.append(key).append('=').append(j3dProps.get(key)).append('\n');
473            }
474
475            buf.append("\n# JYTHON PROPERTIES\n");
476            for (Object key : (new TreeSet<Object>(jythonProps.keySet()))) {
477                buf.append(key).append('=').append(jythonProps.get(key)).append('\n');
478            }
479
480            // get idv/mcv properties
481            buf.append("\n# IDV AND MCIDAS-V PROPERTIES\n");
482            for (String key : (new TreeSet<String>(mcvProps.keySet()))) {
483                buf.append(key).append('=').append(mcvProps.get(key)).append('\n');
484            }
485        }
486        return buf.toString();
487    }
488}