view vm-profiler/jvm-agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/jvm/InstrumentationControl.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.profiler.agent.jvm;

import java.io.BufferedWriter;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.management.ManagementFactory;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

public class InstrumentationControl implements InstrumentationControlMXBean {

    private final Instrumentation instrumentation;
    private final ProfilerInstrumentor classInstrumentor;
    private final ProfileRecorder recorder;
    private final ResultsFileCreator resultsFileCreator;

    private boolean profiling = false;

    private boolean resultsWrittenToDisk = true;
    private String lastResults = null;

    public InstrumentationControl(Instrumentation instrumentation) {
        this(instrumentation, new AsmBasedInstrumentor(), ProfileRecorder.getInstance(), new ResultsFileCreator());
    }

    public InstrumentationControl(Instrumentation instrumentation,
            ProfilerInstrumentor instrumentor,
            ProfileRecorder recorder,
            ResultsFileCreator resultsFileCreator) {
        this.instrumentation = instrumentation;
        this.classInstrumentor = instrumentor;
        this.recorder = recorder;
        this.resultsFileCreator = resultsFileCreator;

        addShutdownHookToSaveData();
    }

    private void addShutdownHookToSaveData() {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                onVmShutdown();
            }
        });
    }

    /** package private for testing */
    void onVmShutdown() {
        writeResultsToDiskIfNotWritten();
    }

    @Override
    public void startProfiling() {
        if (profiling) {
            throw new IllegalStateException("Already started");
        }
        profiling = true;
        resultsWrittenToDisk = false;

        instrumentation.addTransformer(classInstrumentor, true);
        retransformAlreadyLoadedClasses(instrumentation, classInstrumentor);
    }

    @Override
    public void stopProfiling() {
        if (!profiling) {
            throw new IllegalStateException("Not profiling");
        }
        profiling = false;

        instrumentation.removeTransformer(classInstrumentor);
        retransformAlreadyLoadedClasses(instrumentation, classInstrumentor);

        writeProfilingResultsToDisk();
        clearProfileData();
    }

    private void retransformAlreadyLoadedClasses(Instrumentation instrumentation, ProfilerInstrumentor profiler) {
        long start = System.nanoTime();

        List<Class<?>> toTransform = new ArrayList<Class<?>>();

        for (Class<?> klass : instrumentation.getAllLoadedClasses()) {
            boolean skipThisClass = false;
            if (!instrumentation.isModifiableClass(klass)) {
                skipThisClass = true;
            }
            if (!profiler.shouldInstrument(klass)) {
                skipThisClass = true;
            }

            if (skipThisClass) {
                continue;
            }

            toTransform.add(klass);
        }

        if (toTransform.size() > 0) {
            Debug.println("AGENT: Retransforming " + toTransform.size() + " classes");
            for (Class<?> klass : toTransform) {
                try {
                    instrumentation.retransformClasses(klass);
                } catch (UnmodifiableClassException e) {
                    throw new AssertionError("Tried to modify an unmodifiable class", e);
                } catch (Error e) {
                    Debug.printlnError("Failed to transform: " + klass.getName());
                    Debug.printlnError("Unable to retransform all classes. Some classes may not have been instrumented correctly!");
                    Debug.printStackTrace(e);
                }
            }
        }
        long end = System.nanoTime();
        Debug.println("AGENT: Retansforming took: " + (end - start) + "ns");
    }

    private void writeResultsToDiskIfNotWritten() {
        if (!resultsWrittenToDisk) {
            writeProfilingResultsToDisk();
        }
    }

    private void writeProfilingResultsToDisk() {
        try {
            ResultsFile resultsFile = resultsFileCreator.get();
            String path = resultsFile.getPath();
            BufferedWriter out = null;
            try {
                out = resultsFile.getWriter();
                Map<String, AtomicLong> data = recorder.getData();
                Debug.println("AGENT: Writing " + data.size() + " results to: " + path);
                for (Map.Entry<String, AtomicLong> entry : data.entrySet()) {
                    out.write(entry.getValue().get() + "\t" + entry.getKey() + "\n");
                }
                resultsWrittenToDisk = true;
                lastResults = path;
            } finally {
                try {
                    if (out != null) {
                        out.close();
                    }
                } catch (IOException e) {
                    // well, we are screwed
                    Debug.printStackTrace(e);
                }
            }
        } catch (IOException e) {
            Debug.printStackTrace(e);
        }
    }

    private void clearProfileData() {
        recorder.clearData();
    }

    @Override
    public boolean isProfiling() {
        return profiling;
    }

    @Override
    public String getProfilingDataFile() {
        return lastResults;
    }

    static class ResultsFileCreator {

        ResultsFile get() throws IOException {
            Path output = createOutput();
            return new ResultsFile(output);
        }

        private boolean isPosixSystem() {
            final String osname = System.getProperty("os.name").toLowerCase();
            final boolean IS_UNIX = !osname.contains("win");
            return IS_UNIX;
        }

        private Path createOutput() throws IOException {
            if (isPosixSystem()) {
                Set<PosixFilePermission> perm = PosixFilePermissions.fromString("rw-------");
                FileAttribute<Set<PosixFilePermission>> attributes = PosixFilePermissions.asFileAttribute(perm);
                // Include the pid so agent can find it. Surround pid with - to
                // avoid false prefix-based matches. Otherwise the agent searching
                // for "-12" may find "-123" as a valid match.
                return Files.createTempFile("thermostat-" + getProcessId() + "-", ".perfdata", attributes);
            }
            else {
                // should check that this is in a secure location
                return Files.createTempFile("thermostat-" + getProcessId() + "-", ".perfdata");
            }
        }

        private String getProcessId() {
            return ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        }
    }

    static class ResultsFile {

        private final Path path;

        ResultsFile(Path path) { this.path = path; }

        String getPath() { return path.toString(); }

        /** Caller must close the writer when done */
        BufferedWriter getWriter() throws IOException {
            OpenOption[] options = new OpenOption[] {
                    StandardOpenOption.WRITE,
                    StandardOpenOption.TRUNCATE_EXISTING
            };

            return Files.newBufferedWriter(path, StandardCharsets.UTF_8, options);
        }
    }
}