view vm-heap-analysis/common/src/main/java/com/redhat/thermostat/vm/heap/analysis/common/HeapDump.java @ 2598:3d42591a926c

[PATCH] fix profiling and heap dump on Windows Both profiling and heap dump require the creation of files or directories within the user's temp directory (as defined by "java.io.tmp"( Current Thermostat code uses PosixFilePermissions, which fails hard at runtime on Windows. This patch removes the use of PosixFilePermissions on Windows only, allowing Heap Dump to work, and giving Profiling a better chance of working (more work is to be done there; I'm afraid that will be another patch) On Windows, the default temp directory is private to the user, and secure. Further patches will check if java.io.tmpdir is secure, and log a warning if it isn't, and probably allow the user to configure a global directory secured with an ACL if required to debug 'foreign' JVMs. (This means that further work will need to be done to connect to another user's JVM for some Thermostat features that rely on the temp directory) reviewed-by: sgehwolf review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-February/022241.html
author Simon Tooke <stooke@redhat.com>
date Thu, 23 Feb 2017 09:22:15 -0500
parents e7fd8adb5f9c
children
line wrap: on
line source

/*
 * Copyright 2012-2017 Red Hat, Inc.
 *
 * This file is part of Thermostat.
 *
 * Thermostat is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2, or (at your
 * option) any later version.
 *
 * Thermostat is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Thermostat; see the file COPYING.  If not see
 * <http://www.gnu.org/licenses/>.
 *
 * Linking this code with other modules is making a combined work
 * based on this code.  Thus, the terms and conditions of the GNU
 * General Public License cover the whole combination.
 *
 * As a special exception, the copyright holders of this code give
 * you permission to link this code with independent modules to
 * produce an executable, regardless of the license terms of these
 * independent modules, and to copy and distribute the resulting
 * executable under terms of your choice, provided that you also
 * meet, for each linked independent module, the terms and conditions
 * of the license of that module.  An independent module is a module
 * which is not derived from or based on this code.  If you modify
 * this code, you may extend this exception to your version of the
 * library, but you are not obligated to do so.  If you do not wish
 * to do so, delete this exception statement from your version.
 */

package com.redhat.thermostat.vm.heap.analysis.common;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.redhat.thermostat.shared.config.OS;
import org.apache.lucene.analysis.core.SimpleAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.WildcardQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.store.RAMDirectory;

import com.redhat.thermostat.common.utils.LoggingUtils;
import com.redhat.thermostat.vm.heap.analysis.common.model.HeapInfo;
import com.redhat.thermostat.vm.heap.analysis.hat.hprof.model.JavaHeapObject;
import com.redhat.thermostat.vm.heap.analysis.hat.hprof.model.Snapshot;
import com.redhat.thermostat.vm.heap.analysis.hat.hprof.parser.Reader;

/**
 * NOTE: This class is thread-safe with respect to loading the heapdump and creating the index.
 */
public class HeapDump {

    private static final String INDEX_FIELD_OBJECT_ID = "objectId";

    private static final String INDEX_FIELD_CLASSNAME = "classname";

    private static final Logger log = LoggingUtils.getLogger(HeapDump.class);

    private static final int MAX_SEARCH_RESULTS = 1000;

    private final HeapInfo heapInfo;

    private final HeapDAO heapDAO;

    private Snapshot snapshot;

    private Directory luceneIndex;

    // package-private for testing
    HeapDump(HeapInfo heapInfo, HeapDAO heapDAO, Snapshot snapshot) {
        this.heapInfo = heapInfo;
        this.heapDAO = heapDAO;
        this.snapshot = snapshot;
    }
    
    public HeapDump(HeapInfo heapInfo, HeapDAO heapDAO) {
        this(heapInfo, heapDAO, null);
    }

    public long getTimestamp() {
        return heapInfo.getTimeStamp();
    }

    @Override
    public String toString() {
        return "[" + new Date(getTimestamp()) +"] ";
    }

    public ObjectHistogram getHistogram() throws IOException {
        return heapDAO.getHistogram(heapInfo);
    }

    public HeapInfo getInfo() {
        return heapInfo;
    }

    private synchronized Directory getLuceneIndex() {
        if (luceneIndex == null) {
            try {
                luceneIndex = createLuceneIndex();
            } catch (IOException ex) {
                log.log(Level.SEVERE, "Unexpected IO Exception while creating heap dump index", ex);
                return null;
            }
        }
        return luceneIndex;
    }

    // package-private for testing
    Directory createLuceneIndex() throws IOException,
            CorruptIndexException, LockObtainFailedException {

        loadHeapDumpIfNecessary();

        Enumeration<JavaHeapObject> thingos = snapshot.getThings();
        Directory dir = new RAMDirectory();
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(new SimpleAnalyzer());
        IndexWriter writer = new IndexWriter(dir, indexWriterConfig);
        while (thingos.hasMoreElements()) {
            JavaHeapObject thingo = thingos.nextElement();
            Document doc = new Document();
            doc.add(new StringField(INDEX_FIELD_CLASSNAME, thingo.getClazz().getName(), Field.Store.YES));
            doc.add(new StringField(INDEX_FIELD_OBJECT_ID, thingo.getIdString(), Field.Store.YES));
            writer.addDocument(doc);
        }
        writer.close();
        return dir;
    }

    public Snapshot getSnapshot() {
        loadHeapDumpIfNecessary();
        return snapshot;
    }

    private synchronized void loadHeapDumpIfNecessary() {
        if (snapshot == null) {
            try {
                loadHeapDump();
            } catch (IOException e) {
                log.log(Level.SEVERE, "Unexpected IO Exception while loading heap dump", e);
            }
        }
    }

    private void loadHeapDump() throws IOException {
        String filename = "heapdump-" + heapInfo.getHeapId();
        File tmpDir = getOrCreateHeapDumpDir();
        File tmpFile = new File(tmpDir, filename);
        if (! tmpFile.exists()) {
            try (InputStream in = heapDAO.getHeapDumpData(heapInfo);) {
                Files.copy(in, tmpFile.toPath());
            }
            
        }
        snapshot = Reader.readFile(tmpFile.getAbsolutePath(), true, 0);
        snapshot.resolve(true);
    }

    private File getOrCreateHeapDumpDir() throws IOException {
        String dirname = "thermostat-" + System.getProperty("user.name");
        File tmpFile = new File(System.getProperty("java.io.tmpdir"), dirname);
        if (! tmpFile.exists()) {
            if (OS.IS_UNIX) {
                Files.createDirectory(tmpFile.toPath(), PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
            }
            else {
                // windows Java doesn't support PosixFilePermissions class, but the tmpfir is private by default
                Files.createDirectory(tmpFile.toPath());
            }
            return tmpFile;
        } else {
            if (tmpFile.isDirectory()) {
                return tmpFile;
            } else {
                throw new FileAlreadyExistsException(tmpFile.getAbsolutePath());
            }
        }
    }

    /**
     * Find objects with class names matching the given pattern
     * @param wildCardClassNamePattern a case-sensitive wildcard pattern to match class names against
     * @param limit the maximum number of results to return
     * @return a collection of object ids that can be used with {@link #findObject(String)}
     */
    public Collection<String> searchObjects(String wildCardClassNamePattern, int limit) {
        Directory searchIndex = getLuceneIndex();

        WildcardQuery query = new WildcardQuery(new Term(INDEX_FIELD_CLASSNAME, wildCardClassNamePattern));
        Collection<String> results = new ArrayList<String>();
        try {
            IndexReader indexReader = DirectoryReader.open(searchIndex);
            IndexSearcher searcher = new IndexSearcher(indexReader);
            TopDocs found = searcher.search(query, limit);
            for (ScoreDoc scoreDoc : found.scoreDocs) {
                Document doc = searcher.doc(scoreDoc.doc);
                String objectId = doc.get(INDEX_FIELD_OBJECT_ID);
                results.add(objectId);
            }
            indexReader.close();
        } catch (IOException e) {
            log.log(Level.SEVERE, "Unexpected IO Exception while searching heap dump index", e);
        }
        return results;
    }

    public Collection<String> wildcardSearch(String searchText) {
        int limit = Math.min(MAX_SEARCH_RESULTS, searchText.length() * 100);

        String wildCardClassNamePattern = searchText;
        if (!searchText.contains("*") && !searchText.contains("?")) {
            wildCardClassNamePattern = "*" + searchText + "*";
        }
        return searchObjects(wildCardClassNamePattern, limit);
    }

    public JavaHeapObject findObject(String id) {
        loadHeapDumpIfNecessary();
        return snapshot.findThing(id);
    }

    public boolean equals(Object o) {
        return o instanceof HeapDump && ((HeapDump) o).heapInfo.equals(heapInfo);
    }

    public int hashCode() {
        return heapInfo.hashCode();
    }
    
    public String getType() {
        return "hprof";
    }
}