org.micromanager.diagnostics.ProblemReport.java Source code

Java tutorial

Introduction

Here is the source code for org.micromanager.diagnostics.ProblemReport.java

Source

// AUTHOR:       Mark Tsuchida (based in part on previous version by Karl Hoover)
// COPYRIGHT:    University of California, San Francisco, 2010-2014
// LICENSE:      This file is distributed under the BSD license.
//               License text is included with the source distribution.
//               This file 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.
//               IN NO EVENT SHALL THE COPYRIGHT OWNER OR
//               CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
//               INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES.

package org.micromanager.diagnostics;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import mmcorej.CMMCore;

public class ProblemReport {
    private final CMMCore core_;

    private File reportDir_; // null if non-persistent
    private File leftoverDir_;

    // Designed for serialization via GSON.
    private static class Metadata {
        // Used boxed types to allow null
        public Integer pid;
        public Date date;
        public String mmStudioVersion;
        public String startCfgFilename;
        public String endCfgFilename;
        public String userName;
        public String userOrganization;
        public String userEmail;
        public String description;
        public String macAddress;
        public String ipAddress;
        public String hostName;
        public String userLogin;
        public String currentDir;
    }

    private Metadata metadata_;

    private Integer logFileHandle_;
    private String logFileName_;

    private NamedTextFile startCfg_;
    private String capturedLogContent_;
    private NamedTextFile endCfg_;
    private NamedTextFile hotSpotErrorLog_;

    private Timer deferredSyncTimer_ = null;

    // Filenames and strings for persistence
    private static final String LOG_CAPTURE_FILENAME = "CoreLogCapture.txt";
    private static final String START_CFG_FILENAME = "StartConfig.cfg";
    private static final String END_CFG_FILENAME = "EndConfig.cfg";
    private static final String METADATA_FILENAME = "ReportInfo.txt";
    private static final String README_FILENAME = "README.txt";

    /**
     * Create a new problem report.
     *
     * The report will not be backed by persistent storage, and therefore will
     * not be crash-proof.
     *
     * Note that although core is used for logging control, but the information
     * logged may come from global state (the system information classes obtain
     * the Core from the MMStudio singleton).
     *
     * @param core the Core.
     */
    public static ProblemReport NewReport(CMMCore core) {
        return new ProblemReport(core);
    }

    /**
     * Create a new disk-backed report.
     *
     * Note that although core is used for logging control, but the information
     * logged may come from global state (the system information classes obtain
     * the Core from the MMStudio singleton).
     *
     * If there are any errors writing to storageDirectory, they are silently
     * ignored (and the report will behave as if it were non-persistent).
     *
     * @param core the Core.
     * @param storageDirectory where to save the report data.
     */
    public static ProblemReport NewPersistentReport(CMMCore core, File storageDirectory) {
        return new ProblemReport(core, storageDirectory);
    }

    /**
     * Create a report by loading a leftover disk-backed report.
     *
     * @param storageDirectory where to load the report data from.
     */
    public static ProblemReport LoadFromPersistence(File storageDirectory) {
        return new ProblemReport(storageDirectory);
    }

    private ProblemReport(CMMCore core) {
        core_ = core;

        metadata_ = new Metadata();
        metadata_.date = new Date();

        collectHostInformation();
    }

    private ProblemReport(CMMCore core, File storageDirectory) {
        this(core);
        reportDir_ = storageDirectory;
        syncMetadata();
    }

    private ProblemReport(File storageDirectory) {
        core_ = null;
        reportDir_ = null; // No further saving to disk
        loadReport(storageDirectory);
    }

    /**
     * Return true of the report contains anything substantial.
     */
    public boolean isUsefulReport() {
        if (metadata_ == null)
            return false;
        if (capturedLogContent_ != null && !capturedLogContent_.isEmpty())
            return true;
        return false;
    }

    /**
     * Set the user full name.
     *
     * This is for the name entered by the user, not the login name.
     * @param name the name.
     */
    public void setUserName(String name) {
        metadata_.userName = name;
        deferredSyncMetadata();
    }

    /**
     * Get the user full name.
     *
     * This is the human-readable name entered by the user, not the login name.
     * @return the name.
     */
    public String getUserName() {
        return metadata_.userName;
    }

    /**
     * Set the user organization name.
     * @param organization the organization name.
     */
    public void setUserOrganization(String organization) {
        metadata_.userOrganization = organization;
        deferredSyncMetadata();
    }

    /**
     * Get the user organization name.
     * @return the organization name.
     */
    public String getUserOrganization() {
        return metadata_.userOrganization;
    }

    /**
     * Set the user email address.
     * @param email the email address.
     */
    public void setUserEmail(String email) {
        metadata_.userEmail = email;
        deferredSyncMetadata();
    }

    /**
     * Get the user email address.
     * @return the email address.
     */
    public String getUserEmail() {
        return metadata_.userEmail;
    }

    /**
     * Set the user-entered description.
     * @param description the description.
     */
    public void setDescription(String description) {
        metadata_.description = description;
        deferredSyncMetadata();
    }

    /**
     * Get the user-entered description.
     * @return the description.
     */
    public String getDescription() {
        return metadata_.description;
    }

    /**
     * Start CoreLog capture.
     *
     * Save the current hardware configuration file, then start capturing the
     * CoreLog.
     */
    public void startCapturingLog(boolean useCrashRobust) {
        startCfg_ = getCurrentConfigFile();
        syncStartingConfig();

        // Should not happen, but in case we are called erroneously:
        if (logFileHandle_ != null) {
            cancelLogCapture();
        }

        File logFile = null;
        if (reportDir_ != null) {
            logFile = new File(reportDir_, LOG_CAPTURE_FILENAME);
            if (!logFile.exists()) {
                // Touch, so that we can test canWrite below
                try {
                    new FileOutputStream(logFile).close();
                } catch (java.io.FileNotFoundException dealWithLater) {
                } catch (java.io.IOException ignore) {
                }
            }
        } else {
            try {
                logFile = File.createTempFile("MMCoreLogCapture", ".txt");
            } catch (java.io.IOException dealWithLater) {
            }
        }
        if (logFile == null || !logFile.canWrite()) {
            capturedLogContent_ = "<<<Cannot write to temporary file for log capture>>>";
            return;
        }
        String filename = logFile.getAbsolutePath();

        try {
            logFileHandle_ = core_.startSecondaryLogFile(filename, true, true, useCrashRobust);
        } catch (Exception e) {
            capturedLogContent_ = "<<<Failed to start log capture>>>";
        }
        logFileName_ = filename;
        core_.logMessage("Problem Report: Start of log capture");
    }

    /**
     * Stop CoreLog capture and discard captured log.
     */
    public void cancelLogCapture() {
        if (logFileHandle_ == null) {
            return;
        }

        core_.logMessage("Problem Report: Canceling log capture");
        try {
            core_.stopSecondaryLogFile(logFileHandle_);
        } catch (Exception ignore) {
            // Errors will be logged by the Core; there is not much else we can
            // do.
        }
        logFileHandle_ = null;

        new File(logFileName_).delete();
        logFileName_ = null;
    }

    /**
     * Stop CoreLog capture and record the captured log as part of the report.
     *
     * After stopping CoreLog capture, the current hardware configuration file
     * is also recorded.
     */
    public void finishCapturingLog() {
        if (logFileHandle_ == null) {
            // Either starting the capture failed, or we were erroneously called
            // when capture is not running.
            endCfg_ = getCurrentConfigFile();
            syncEndingConfig();
            return;
        }

        String logFileName = logFileName_;

        core_.logMessage("Problem Report: End of log capture");
        try {
            core_.stopSecondaryLogFile(logFileHandle_);
        } catch (Exception ignore) {
            // This is an unlikely error unless there are programming errors.
            // Let's continue and see if we can read the file anyway.
        }
        logFileHandle_ = null;
        logFileName_ = null;

        java.io.File logFile = new java.io.File(logFileName);
        capturedLogContent_ = readTextFile(logFile);
        if (capturedLogContent_ == null) {
            capturedLogContent_ = "<<<Failed to read captured log file>>>";
        }
        if (reportDir_ == null) { // We used an ad-hoc temporary file
            logFile.delete();
        }

        endCfg_ = getCurrentConfigFile();
        syncEndingConfig();
    }

    public void deleteStorage() {
        deleteReportDir(reportDir_);
        reportDir_ = null;
        deleteReportDir(leftoverDir_);
        leftoverDir_ = null;
    }

    /**
     * Dump system information to the CoreLog.
     *
     * This is intended to be called after startCapturingLog().
     *
     * @param incremental if true, only dump info that is likely to change.
     */
    public void logSystemInfo(boolean incremental) {
        final String inc = incremental ? " (incremental)" : "";
        core_.logMessage("***** BEGIN Problem Report System Info" + inc + " *****");
        SystemInfo.dumpAllToCoreLog(!incremental);
        core_.logMessage("***** END Problem Report System Info" + inc + " *****");
    }

    public void logUserComment(String comment) {
        core_.logMessage("##### User remark: " + comment);
    }

    /*
     * Package-private accessors used by report formatter
     */

    boolean hasStartingConfig() {
        return startCfg_ != null;
    }

    boolean hasEndingConfig() {
        return endCfg_ != null;
    }

    boolean configChangedDuringLogCapture() {
        if (startCfg_ == null || endCfg_ == null) {
            return startCfg_ != endCfg_;
        }
        return !startCfg_.equals(endCfg_);
    }

    String getStartingConfigFileName() {
        if (startCfg_ == null) {
            return null;
        }
        return startCfg_.getFilename();
    }

    String getEndingConfigFileName() {
        if (endCfg_ == null) {
            return null;
        }
        return endCfg_.getFilename();
    }

    String getStartingConfig() {
        if (startCfg_ == null) {
            return null;
        }
        return startCfg_.getContent();
    }

    String getEndingConfig() {
        if (endCfg_ == null) {
            return null;
        }
        return endCfg_.getContent();
    }

    String getCapturedLogContent() {
        return capturedLogContent_;
    }

    boolean hasHotSpotErrorLog() {
        return hotSpotErrorLog_ != null;
    }

    String getHotSpotErrorLogFileName() {
        if (hotSpotErrorLog_ == null) {
            return null;
        }
        return hotSpotErrorLog_.getFilename();
    }

    String getHotSpotErrorLogContent() {
        if (hotSpotErrorLog_ == null) {
            return null;
        }
        return hotSpotErrorLog_.getContent();
    }

    String getMACAddress() {
        return metadata_.macAddress;
    }

    String getHostName() {
        return metadata_.hostName;
    }

    String getIPAddress() {
        return metadata_.ipAddress;
    }

    String getUserId() {
        return metadata_.userLogin;
    }

    int getPid() {
        return metadata_.pid;
    }

    Date getDate() {
        return (Date) metadata_.date.clone();
    }

    /*
     * Private helper methods and classes
     */

    private void collectHostInformation() {
        java.lang.management.RuntimeMXBean rtMXB = java.lang.management.ManagementFactory.getRuntimeMXBean();
        final String jvmName = rtMXB.getName();
        try {
            metadata_.pid = Integer.parseInt(jvmName.split("@")[0]);
        } catch (NumberFormatException e) {
            metadata_.pid = null;
        }

        metadata_.macAddress = null;
        mmcorej.StrVector addrs = core_.getMACAddresses();
        if (addrs.size() > 0) {
            String addr = addrs.get(0);
            if (addr.length() > 0) {
                metadata_.macAddress = addr;
            }
        }

        metadata_.hostName = null;
        try {
            metadata_.hostName = java.net.InetAddress.getLocalHost().getHostName();
        } catch (java.io.IOException ignore) {
        }

        metadata_.ipAddress = null;
        try {
            metadata_.ipAddress = java.net.InetAddress.getLocalHost().getHostAddress();
        } catch (java.io.IOException ignore) {
        }

        metadata_.userLogin = core_.getUserId();
        metadata_.currentDir = System.getProperty("user.dir");
    }

    private static String readTextFile(java.io.File file) {
        // Important: do NOT try to use java.nio mapped file channel to read.
        // Windows will not be able to delete a file once it has been mapped in
        // the current process, no matter how correctly we "close" it.
        Reader reader = null;
        try {
            reader = new FileReader(file);
        } catch (java.io.FileNotFoundException e) {
            return e.getMessage();
        }
        StringBuilder sb = new StringBuilder();
        try {
            int read;
            char[] buf = new char[8192];
            while ((read = reader.read(buf)) > 0) {
                sb.append(buf, 0, read);
            }
        } catch (java.io.IOException e) {
            return e.getMessage();
        } finally {
            try {
                reader.close();
            } catch (java.io.IOException ignore) {
            }
        }
        return sb.toString();
    }

    private static void writeTextFile(java.io.File file, String text) {
        FileOutputStream outputStream;
        try {
            outputStream = new FileOutputStream(file);
        } catch (java.io.FileNotFoundException e) {
            return;
        }
        OutputStreamWriter writer = null;
        try {
            writer = new OutputStreamWriter(outputStream, "UTF-8");
        } catch (java.io.UnsupportedEncodingException wontHappen) {
            // "UTF-8" is guaranteed to be supported.
        }
        try {
            writer.write(text);
        } catch (java.io.IOException e) {
            return;
        } finally {
            try {
                writer.close();
            } catch (java.io.IOException ignore) {
            }
        }
    }

    private void createReportDir() {
        if (reportDir_ == null) {
            return;
        }

        if (reportDir_.mkdirs()) {
            File readmeFile = new File(reportDir_, README_FILENAME);
            if (!readmeFile.isFile()) {
                String readme = "This directory contains an in-progress (or crashed) \n"
                        + "Micro-Manager Problem Report. It is safe to delete.";
                writeTextFile(readmeFile, readme);
            }
        }
        // Ignore errors.
    }

    private void deleteReportDir(File directory) {
        if (directory != null) {
            new File(directory, LOG_CAPTURE_FILENAME).delete();
            new File(directory, START_CFG_FILENAME).delete();
            new File(directory, END_CFG_FILENAME).delete();
            new File(directory, METADATA_FILENAME).delete();
            new File(directory, README_FILENAME).delete();
            directory.delete();
        }
    }

    private Gson makeGson() {
        final DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

        class MyDateSerializer implements JsonSerializer<Date> {
            @Override
            public JsonElement serialize(Date src, Type srcType, JsonSerializationContext context) {
                return new JsonPrimitive(format.format(src));
            }
        }

        class MyDateDeserializer implements JsonDeserializer<Date> {
            @Override
            public Date deserialize(JsonElement json, Type dstType, JsonDeserializationContext context)
                    throws JsonParseException {
                try {
                    return format.parse(json.getAsJsonPrimitive().getAsString());
                } catch (java.text.ParseException e) {
                    return null;
                }
            }
        }

        return new GsonBuilder().registerTypeAdapter(Date.class, new MyDateSerializer())
                .registerTypeAdapter(Date.class, new MyDateDeserializer()).create();
    }

    private synchronized void deferredSyncMetadata() {
        if (reportDir_ == null) {
            return;
        }

        if (deferredSyncTimer_ == null) {
            TimerTask task = new TimerTask() {
                @Override
                public void run() {
                    syncMetadata();
                }
            };
            deferredSyncTimer_ = new Timer("ProblemReportMetadataSync", true);
            deferredSyncTimer_.schedule(task, 1000);
        }
    }

    private synchronized void syncMetadata() {
        if (reportDir_ == null) {
            return;
        }
        createReportDir();

        // I wanted to first write to a temp file and then atomically rename it
        // to the destination file. But Windows fails to rename even if I delete
        // the destination file just before the rename (it does work if there is
        // some pause between the delete and rename, but that is not a viable
        // workaround). So I'm giving up and just overwriting the file.
        File metadataFile = new File(reportDir_, METADATA_FILENAME);
        Gson gson = makeGson();
        writeTextFile(metadataFile, gson.toJson(metadata_));

        if (deferredSyncTimer_ != null) {
            deferredSyncTimer_.cancel();
            deferredSyncTimer_ = null;
        }
    }

    private void syncStartingConfig() {
        if (reportDir_ == null) {
            return;
        }
        createReportDir();

        if (startCfg_ != null) {
            metadata_.startCfgFilename = startCfg_.getFilename();
            syncMetadata();
            writeTextFile(new File(reportDir_, START_CFG_FILENAME), startCfg_.getContent());
        } else if (metadata_.startCfgFilename != null) {
            new File(reportDir_, START_CFG_FILENAME).delete();
            metadata_.startCfgFilename = null;
        }
    }

    private void syncEndingConfig() {
        if (reportDir_ == null) {
            return;
        }
        createReportDir();

        if (endCfg_ != null) {
            metadata_.endCfgFilename = endCfg_.getFilename();
            syncMetadata();
            writeTextFile(new File(reportDir_, END_CFG_FILENAME), endCfg_.getContent());
        } else if (metadata_.endCfgFilename != null) {
            new File(reportDir_, END_CFG_FILENAME).delete();
            metadata_.endCfgFilename = null;
        }
    }

    private void loadReport(File directory) {
        if (!directory.isDirectory()) {
            return;
        }

        leftoverDir_ = directory;

        File metadataFile = new File(directory, METADATA_FILENAME);
        if (!metadataFile.isFile()) {
            return;
        }
        String metadataJson = readTextFile(metadataFile);
        if (metadataJson == null) {
            return;
        }
        Gson gson = makeGson();
        metadata_ = gson.fromJson(metadataJson, Metadata.class);

        if (metadata_.startCfgFilename != null) {
            startCfg_ = new NamedTextFile(metadata_.startCfgFilename, new File(directory, START_CFG_FILENAME));
        }

        if (metadata_.endCfgFilename != null) {
            endCfg_ = new NamedTextFile(metadata_.endCfgFilename, new File(directory, END_CFG_FILENAME));
        }

        capturedLogContent_ = readTextFile(new File(directory, LOG_CAPTURE_FILENAME));

        if (metadata_.pid != null) {
            loadHotSpotErrorLogForPid(metadata_.pid);
        }
    }

    private void loadHotSpotErrorLogForPid(int pid) {
        String logFilename = "hs_err_pid" + Integer.toString(pid) + ".log";
        File logDir;
        if (metadata_.currentDir != null) {
            logDir = new File(metadata_.currentDir);
        } else {
            // Try the current current directory, in case we get lucky.
            logDir = new File(System.getProperty("user.dir"));
        }
        File logFile = new File(logDir, logFilename);
        if (logFile.isFile()) {
            hotSpotErrorLog_ = new NamedTextFile(logFile);
        }
    }

    private static NamedTextFile getCurrentConfigFile() {
        String fileName = org.micromanager.MMStudio.getInstance().getSysConfigFile();
        if (fileName == null || fileName.isEmpty()) {
            return null;
        }
        return new NamedTextFile(fileName);
    }

    // A text file's name and content.
    private static class NamedTextFile {
        final private String filename_;
        final private String content_;

        public NamedTextFile(String filename) {
            this(filename, new File(filename));
        }

        public NamedTextFile(File file) {
            this(file.getAbsolutePath(), file);
        }

        public NamedTextFile(String filename, File file) {
            filename_ = filename;
            content_ = readTextFile(file);
        }

        public boolean equals(NamedTextFile rhs) {
            if (this == rhs) {
                return true;
            }

            if (!filename_.equals(rhs.filename_)) {
                return false;
            }

            return content_.equals(rhs.content_);
        }

        public String getFilename() {
            return filename_;
        }

        public String getContent() {
            return content_;
        }
    }
}