changeset 1585:574c6023b53f

Clean up classes in vm-profiler agent Reviewed-by: vanaltj Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2014-December/011891.html
author Omair Majid <omajid@redhat.com>
date Wed, 03 Dec 2014 11:53:17 -0500
parents f2a417f1cc51
children 67b1c0156c52
files vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/Activator.java vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiver.java vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerException.java vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerRequestReceiver.java vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerVmStatusListener.java vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/RemoteProfilerCommunicator.java vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/VmIdToPidMapper.java vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/VmProfiler.java vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ActivatorTest.java vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiverTest.java vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerRequestReceiverTest.java vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerVmStatusListenerTest.java vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/RemoteProfilerCommunicatorTest.java vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/VmProfilerTest.java vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/ProfileRequest.java
diffstat 15 files changed, 956 insertions(+), 514 deletions(-) [+]
line wrap: on
line diff
--- a/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/Activator.java	Wed Dec 03 16:01:36 2014 +0100
+++ b/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/Activator.java	Wed Dec 03 11:53:17 2014 -0500
@@ -66,16 +66,18 @@
         tracker = new MultipleServiceTracker(context, deps, new MultipleServiceTracker.Action() {
             private ReceiverRegistry requestHandlerRegisteration;
             private VmStatusListenerRegistrar vmStatusRegistrar;
-            private ProfileVmRequestReceiver profileRequestHandler;
+            private ProfilerRequestReceiver profileRequestHandler;
+            private ProfilerVmStatusListener livenessListener;
 
             @Override
             public void dependenciesUnavailable() {
                 requestHandlerRegisteration.unregisterReceivers();
                 requestHandlerRegisteration = null;
+                profileRequestHandler = null;
 
-                vmStatusRegistrar.unregister(profileRequestHandler);
+                vmStatusRegistrar.unregister(livenessListener);
+                livenessListener = null;
 
-                profileRequestHandler = null;
             }
 
             @Override
@@ -85,14 +87,15 @@
                 ProfileDAO dao = get(ProfileDAO.class, services);
                 String writerId = writerIdProvider.getWriterID();
 
-                VmProfiler profiler = new VmProfiler(configuration, pool);
-                profileRequestHandler = new ProfileVmRequestReceiver(writerId, profiler, dao);
+                VmProfiler profiler = new VmProfiler(writerId, configuration, dao, pool);
+                profileRequestHandler = new ProfilerRequestReceiver(profiler);
 
                 requestHandlerRegisteration = new ReceiverRegistry(context);
                 requestHandlerRegisteration.registerReceiver(profileRequestHandler);
 
+                livenessListener = new ProfilerVmStatusListener(profiler);
                 vmStatusRegistrar = new VmStatusListenerRegistrar(context);
-                vmStatusRegistrar.register(profileRequestHandler);
+                vmStatusRegistrar.register(livenessListener);
             }
             private <T> T get(Class<T> klass, Map<String, Object> services) {
                 return (T) services.get(klass.getName());
--- a/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiver.java	Wed Dec 03 16:01:36 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,214 +0,0 @@
-/*
- * Copyright 2012-2014 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.internal;
-
-import java.io.File;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import com.redhat.thermostat.agent.VmStatusListener;
-import com.redhat.thermostat.agent.command.RequestReceiver;
-import com.redhat.thermostat.common.Clock;
-import com.redhat.thermostat.common.SystemClock;
-import com.redhat.thermostat.common.command.Request;
-import com.redhat.thermostat.common.command.Response;
-import com.redhat.thermostat.common.command.Response.ResponseType;
-import com.redhat.thermostat.common.utils.LoggingUtils;
-import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
-import com.redhat.thermostat.vm.profiler.common.ProfileRequest;
-import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
-
-public class ProfileVmRequestReceiver implements RequestReceiver, VmStatusListener {
-
-    private static final Logger logger = LoggingUtils.getLogger(ProfileVmRequestReceiver.class);
-
-    /** A pid that corresponds to an unknown */
-    private static final int UNKNOWN_VMID = -1;
-
-    private static final Response OK = new Response(ResponseType.OK);
-    private static final Response ERROR = new Response(ResponseType.NOK);
-
-    static class FileTimeStampLatestFirst implements Comparator<File> {
-        @Override
-        public int compare(File o1, File o2) {
-            return Long.compare(o2.lastModified(), o1.lastModified());
-        }
-    }
-
-    static class ProfileUploaderCreator {
-        ProfileUploader create(ProfileDAO dao, String agentId, String vmId, int pid) {
-            return new ProfileUploader(dao, agentId, vmId, pid);
-        }
-    }
-
-    private final ConcurrentHashMap<String, Integer> vmIdToPid = new ConcurrentHashMap<>();
-
-    private final String agentId;
-
-    private final Clock clock;
-    private final VmProfiler profiler;
-    private final ProfileDAO dao;
-    private ProfileUploaderCreator uploaderCreator;
-
-    private List<Integer> currentlyProfiledVms = new ArrayList<>();
-
-    public ProfileVmRequestReceiver(String agentId, VmProfiler profiler, ProfileDAO dao) {
-        this(agentId, new SystemClock(), profiler, dao, new ProfileUploaderCreator());
-    }
-
-    public ProfileVmRequestReceiver(String agentId, Clock clock, VmProfiler profiler, ProfileDAO dao, ProfileUploaderCreator uploaderCreator) {
-        this.clock = clock;
-        this.profiler = profiler;
-        this.dao = dao;
-        this.uploaderCreator = uploaderCreator;
-
-        this.agentId = agentId;
-    }
-
-    @Override
-    public synchronized void vmStatusChanged(Status newStatus, String vmId, int pid) {
-        if (newStatus == Status.VM_ACTIVE || newStatus == Status.VM_STARTED) {
-            // TODO assert not already being profiled
-            vmIdToPid.putIfAbsent(vmId, pid);
-        } else {
-            disableProfilerIfActive(vmId, pid);
-            vmIdToPid.remove(vmId, pid);
-        }
-    }
-
-    private void disableProfilerIfActive(String vmId, int pid) {
-        if (currentlyProfiledVms.contains(pid)) {
-            stopProfiling(vmId, pid, false);
-        }
-    }
-
-    @Override
-    public synchronized Response receive(Request request) {
-        String value = request.getParameter(ProfileRequest.PROFILE_ACTION);
-        String vmId = request.getParameter(ProfileRequest.VM_ID);
-
-        int pid = getPid(vmId);
-        if (pid == UNKNOWN_VMID) {
-            logger.warning("Unknown vmId: " + vmId + ". Known vmIds are : " + vmIdToPid.keySet().toString());
-            return ERROR;
-        }
-
-        switch (value) {
-        case ProfileRequest.START_PROFILING:
-            return startProfiling(vmId, pid);
-        case ProfileRequest.STOP_PROFILING:
-            return stopProfiling(vmId, pid, true);
-        default:
-            logger.warning("Unknown command: '" + value + "'");
-            return ERROR;
-        }
-    }
-
-    /** Return the pid or {@link #UNKNOWN_VMID} */
-    private int getPid(String vmId) {
-        Integer pid = vmIdToPid.get(vmId);
-        if (pid == null) {
-            return UNKNOWN_VMID;
-        } else {
-            return pid;
-        }
-    }
-
-    private Response startProfiling(String vmId, int pid) {
-        logger.info("Starting profiling " + pid);
-        try {
-            profiler.startProfiling(pid);
-            currentlyProfiledVms.add(pid);
-            dao.addStatus(new ProfileStatusChange(agentId, vmId, clock.getRealTimeMillis(), true));
-            return OK;
-        } catch (Exception e) {
-            logger.log(Level.INFO, "start profiling failed", e);
-            return ERROR;
-        }
-    }
-
-    private Response stopProfiling(String vmId, int pid, boolean alive) {
-        logger.info("Stopping profiling " + pid);
-        try {
-            ProfileUploader uploader = uploaderCreator.create(dao, agentId, vmId, pid);
-            if (alive) {
-                // if the VM is alive, communicate with it directly
-                profiler.stopProfiling(pid, uploader);
-            } else {
-                findAndUploadProfilingResultsStoredOnDisk(pid, uploader);
-            }
-            dao.addStatus(new ProfileStatusChange(agentId, vmId, clock.getRealTimeMillis(), false));
-            currentlyProfiledVms.remove((Integer) pid);
-            return OK;
-        } catch (Exception e) {
-            logger.log(Level.INFO, "stop profiling failed", e);
-            return ERROR;
-        }
-    }
-
-    private void findAndUploadProfilingResultsStoredOnDisk(final int pid, ProfileUploader uploader) throws IOException {
-        long timeStamp = clock.getRealTimeMillis();
-        // look for latest profiling data that it might have emitted on shutdown
-        File file = findProfilingResultFile(pid);
-        uploader.upload(timeStamp, file);
-    }
-
-    private File findProfilingResultFile(final int pid) {
-        // from InstrumentationControl:
-        // return Files.createTempFile("thermostat-" + getProcessId(), ".perfdata", attributes);
-        String tmpDir = System.getProperty("java.io.tmpdir");
-        File[] files = new File(tmpDir).listFiles(new FilenameFilter() {
-            @Override
-            public boolean accept(File dir, String name) {
-                return name.startsWith("thermostat-" + pid + "-") && name.endsWith(".perfdata");
-            }
-        });
-
-        List<File> filesSortedByTimeStamp = Arrays.asList(files);
-        Collections.sort(filesSortedByTimeStamp, new FileTimeStampLatestFirst());
-        return filesSortedByTimeStamp.get(0);
-    }
-
-}
--- a/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerException.java	Wed Dec 03 16:01:36 2014 +0100
+++ b/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerException.java	Wed Dec 03 11:53:17 2014 -0500
@@ -38,7 +38,12 @@
 
 public class ProfilerException extends Exception {
 
+    public ProfilerException(String message) {
+        super(message);
+    }
+
     public ProfilerException(String message, Exception cause) {
         super(message, cause);
     }
+
 }
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerRequestReceiver.java	Wed Dec 03 11:53:17 2014 -0500
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2012-2014 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.internal;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.redhat.thermostat.agent.command.RequestReceiver;
+import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.Response;
+import com.redhat.thermostat.common.command.Response.ResponseType;
+import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.vm.profiler.common.ProfileRequest;
+
+public class ProfilerRequestReceiver implements RequestReceiver {
+
+    private static final Logger logger = LoggingUtils.getLogger(ProfilerRequestReceiver.class);
+
+    private static final Response OK = new Response(ResponseType.OK);
+    private static final Response ERROR = new Response(ResponseType.NOK);
+
+    private final VmProfiler profiler;
+
+    public ProfilerRequestReceiver(VmProfiler profiler) {
+        this.profiler = profiler;
+    }
+
+    @Override
+    public Response receive(Request request) {
+        String value = request.getParameter(ProfileRequest.PROFILE_ACTION);
+        String vmId = request.getParameter(ProfileRequest.VM_ID);
+
+        try {
+            switch (value) {
+            case ProfileRequest.START_PROFILING:
+                return startProfiling(vmId);
+            case ProfileRequest.STOP_PROFILING:
+                return stopProfiling(vmId);
+            default:
+                logger.warning("Unknown command: '" + value + "'");
+                return ERROR;
+            }
+        } catch (Exception e) {
+            logger.log(Level.FINE, "Exception handling command", e);
+            return ERROR;
+        }
+    }
+
+    private Response startProfiling(String vmId) {
+        logger.info("Starting profiling " + vmId);
+        try {
+            profiler.startProfiling(vmId);
+            return OK;
+        } catch (Exception e) {
+            logger.log(Level.INFO, "start profiling failed", e);
+            return ERROR;
+        }
+    }
+
+    private Response stopProfiling(String vmId) {
+        logger.info("Stopping profiling " + vmId);
+        try {
+            profiler.stopProfiling(vmId);
+            return OK;
+        } catch (Exception e) {
+            logger.log(Level.INFO, "stop profiling failed", e);
+            return ERROR;
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerVmStatusListener.java	Wed Dec 03 11:53:17 2014 -0500
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012-2014 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.internal;
+
+import com.redhat.thermostat.agent.VmStatusListener;
+
+public class ProfilerVmStatusListener implements VmStatusListener {
+
+    private final VmProfiler profiler;
+
+    public ProfilerVmStatusListener(VmProfiler profiler) {
+        this.profiler = profiler;
+    }
+
+    @Override
+    public void vmStatusChanged(Status newStatus, String vmId, int pid) {
+        if (newStatus == Status.VM_ACTIVE || newStatus == Status.VM_STARTED) {
+            profiler.vmStarted(vmId, pid);
+        } else {
+            profiler.vmStopped(vmId, pid);
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/RemoteProfilerCommunicator.java	Wed Dec 03 11:53:17 2014 -0500
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2012-2014 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.internal;
+
+import java.io.IOException;
+
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+
+import com.redhat.thermostat.agent.utils.management.MXBeanConnection;
+import com.redhat.thermostat.agent.utils.management.MXBeanConnectionPool;
+import com.sun.tools.attach.AgentInitializationException;
+import com.sun.tools.attach.AgentLoadException;
+import com.sun.tools.attach.AttachNotSupportedException;
+import com.sun.tools.attach.VirtualMachine;
+
+class RemoteProfilerCommunicator {
+
+    // TODO should this maintain enough state to know when to attach and load agent?
+
+    // published by the jvm-agent
+    private static final String INSTRUMENTATION_OBJECT = "com.redhat.thermostat:type=InstrumentationControl";
+
+    static class Attacher {
+        VirtualMachine attach(String pid) throws AttachNotSupportedException, IOException {
+            return VirtualMachine.attach(pid);
+        }
+    }
+
+    private MXBeanConnectionPool connectionPool;
+    private final Attacher attacher;
+
+    public RemoteProfilerCommunicator(MXBeanConnectionPool connectionPool) {
+        this(connectionPool, new Attacher());
+    }
+
+    public RemoteProfilerCommunicator(MXBeanConnectionPool connectionPool, Attacher attacher) {
+        this.connectionPool = connectionPool;
+        this.attacher = attacher;
+    }
+
+    public void loadAgentIntoPid(int pid, String jar, String options) throws ProfilerException {
+        try {
+            VirtualMachine vm = attacher.attach(String.valueOf(pid));
+            try {
+                vm.loadAgent(jar, options);
+            } catch (AgentLoadException | AgentInitializationException e) {
+                throw new ProfilerException("Error starting profiler", e);
+            } finally {
+                vm.detach();
+            }
+        } catch (IOException | AttachNotSupportedException e) {
+            throw new ProfilerException("Error starting profiler", e);
+        }
+    }
+
+    public void startProfiling(int pid) throws ProfilerException {
+        invokeMethodOnInstrumentation(pid, "startProfiling");
+    }
+
+    public void stopProfiling(int pid) throws ProfilerException {
+        invokeMethodOnInstrumentation(pid, "stopProfiling");
+    }
+
+    public String getProfilingDataFile(int pid) throws ProfilerException {
+        return (String) getInstrumentationAttribute(pid, "ProfilingDataFile");
+    }
+
+    private Object invokeMethodOnInstrumentation(int pid, String name) throws ProfilerException {
+        try {
+            MXBeanConnection connection = connectionPool.acquire(pid);
+            try {
+                ObjectName instrumentation = new ObjectName(INSTRUMENTATION_OBJECT);
+                MBeanServerConnection server = connection.get();
+                return server.invoke(instrumentation, name, new Object[0], new String[0]);
+            } finally {
+                connectionPool.release(pid, connection);
+            }
+        } catch (Exception e) {
+            throw new ProfilerException("Unable to communicate with remote profiler", e);
+        }
+    }
+
+    private Object getInstrumentationAttribute(int pid, String name) throws ProfilerException {
+        try {
+            MXBeanConnection connection = connectionPool.acquire(pid);
+            try {
+                ObjectName instrumentation = new ObjectName(INSTRUMENTATION_OBJECT);
+                MBeanServerConnection server = connection.get();
+                return server.getAttribute(instrumentation, name);
+            } finally {
+                connectionPool.release(pid, connection);
+            }
+        } catch (Exception e) {
+            throw new ProfilerException("Unable to communicate with remote profiler", e);
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/VmIdToPidMapper.java	Wed Dec 03 11:53:17 2014 -0500
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012-2014 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.internal;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+class VmIdToPidMapper {
+
+    /** A pid that corresponds to an unknown */
+    static final int UNKNOWN_VMID = -1;
+
+    private final ConcurrentHashMap<String, Integer> vmIdToPid = new ConcurrentHashMap<>();
+
+    void add(String vmId, int pid) {
+        vmIdToPid.putIfAbsent(vmId, pid);
+    }
+
+    /** Return the pid or {@link #UNKNOWN_VMID} */
+    int getPid(String vmId) {
+        Integer pid = vmIdToPid.get(vmId);
+        if (pid == null) {
+            return UNKNOWN_VMID;
+        } else {
+            return pid;
+        }
+    }
+
+    void remove(String vmId, int pid) {
+        vmIdToPid.remove(vmId, pid);
+    }
+
+}
--- a/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/VmProfiler.java	Wed Dec 03 16:01:36 2014 +0100
+++ b/vm-profiler/agent/src/main/java/com/redhat/thermostat/vm/profiler/agent/internal/VmProfiler.java	Wed Dec 03 11:53:17 2014 -0500
@@ -37,116 +37,178 @@
 package com.redhat.thermostat.vm.profiler.agent.internal;
 
 import java.io.File;
+import java.io.FilenameFilter;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 import java.util.Objects;
 import java.util.Properties;
 import java.util.logging.Logger;
 
-import javax.management.MBeanServerConnection;
-import javax.management.ObjectName;
-
-import com.redhat.thermostat.agent.utils.management.MXBeanConnection;
 import com.redhat.thermostat.agent.utils.management.MXBeanConnectionPool;
 import com.redhat.thermostat.common.Clock;
 import com.redhat.thermostat.common.SystemClock;
 import com.redhat.thermostat.common.utils.LoggingUtils;
-import com.sun.tools.attach.AgentInitializationException;
-import com.sun.tools.attach.AgentLoadException;
-import com.sun.tools.attach.AttachNotSupportedException;
-import com.sun.tools.attach.VirtualMachine;
+import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
+import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
 
 public class VmProfiler {
 
+    static class MostRecentFileFirst implements Comparator<File> {
+        @Override
+        public int compare(File o1, File o2) {
+            return Long.compare(o2.lastModified(), o1.lastModified());
+        }
+    }
+
+    static class ProfileUploaderCreator {
+        ProfileUploader create(ProfileDAO dao, String agentId, String vmId, int pid) {
+            return new ProfileUploader(dao, agentId, vmId, pid);
+        }
+    }
+
     private static final Logger logger = LoggingUtils.getLogger(VmProfiler.class);
 
-    private final MXBeanConnectionPool connectionPool;
-    private final Attacher attacher;
-    private final Clock clock;
+    private final List<Integer> currentlyProfiledVmPids = new ArrayList<>();
+
+    private final VmIdToPidMapper vmIdToPid = new VmIdToPidMapper();
 
-    private String agentJarPath;
-    private String asmJarPath;
+    private final String agentId;
+    private final Clock clock;
+    private final ProfileDAO dao;
+    private final ProfileUploaderCreator uploaderCreator;
+    private final RemoteProfilerCommunicator remote;
 
-    public VmProfiler(Properties configuration, MXBeanConnectionPool connectionPool) {
-        this(configuration, connectionPool, new Attacher(), new SystemClock());
+    private final String agentJarPath;
+    private final String asmJarPath;
+
+    public VmProfiler(String agentId, Properties configuration, ProfileDAO dao, MXBeanConnectionPool pool) {
+        this(agentId, configuration, dao, new SystemClock(), new ProfileUploaderCreator(), new RemoteProfilerCommunicator(pool));
     }
 
-    public VmProfiler(Properties configuration, MXBeanConnectionPool connectionPool, Attacher attacher, Clock clock) {
-        this.connectionPool = connectionPool;
-        this.attacher = attacher;
+    VmProfiler(String agentId, Properties configuration,
+            ProfileDAO dao,
+            Clock clock, ProfileUploaderCreator creator, RemoteProfilerCommunicator remote) {
+        this.agentId = agentId;
         this.clock = clock;
+        this.dao = dao;
+        this.uploaderCreator = creator;
+        this.remote = remote;
 
         // requireNonNull protects against bad config with missing values
         agentJarPath = Objects.requireNonNull(configuration.getProperty("AGENT_JAR"));
         asmJarPath = Objects.requireNonNull(configuration.getProperty("ASM_JAR"));
     }
 
-    public void startProfiling(int pid) throws ProfilerException {
-        loadProfilerAgentIntoPid(pid);
-
-        invokeMethodOnInstrumentation(pid, "startProfiling");
+    public synchronized void vmStarted(String vmId, int pid) {
+        // assert not already being profiled
+        if (currentlyProfiledVmPids.contains((Integer) pid)) {
+            throw new IllegalStateException("VM " + pid + " is already being profiled");
+        }
+        vmIdToPid.add(vmId, pid);
     }
 
-    private void loadProfilerAgentIntoPid(int pid) throws ProfilerException {
+    public synchronized void vmStopped(String vmId, int pid) {
         try {
-            VirtualMachine vm = attacher.attach(String.valueOf(pid));
-            try {
-                String jarsToLoad = ""; // asmJarPath + ":" + agentJarPath;
-                logger.info("Asking " + pid + " to load agent '" + agentJarPath + "' with arguments '" + jarsToLoad + "'");
-                vm.loadAgent(agentJarPath, jarsToLoad);
-            } catch (AgentLoadException | AgentInitializationException e) {
-                throw new ProfilerException("Error starting profiler", e);
-            } finally {
-                vm.detach();
-            }
-        } catch (IOException | AttachNotSupportedException e) {
-            throw new ProfilerException("Error starting profiler", e);
+            disableProfilerIfActive(vmId, pid);
+        } catch (ProfilerException e) {
+            logger.warning(e.getMessage());
+        }
+        vmIdToPid.remove(vmId, pid);
+    }
+
+    private void disableProfilerIfActive(String vmId, int pid) throws ProfilerException {
+        if (currentlyProfiledVmPids.contains(pid)) {
+            stopProfiling(vmId, false);
         }
     }
 
-    public void stopProfiling(int pid, ProfileUploader uploader) throws ProfilerException {
-        invokeMethodOnInstrumentation(pid, "stopProfiling");
+    public synchronized void startProfiling(String vmId) throws ProfilerException {
+        int pid = vmIdToPid.getPid(vmId);
+        if (pid == VmIdToPidMapper.UNKNOWN_VMID) {
+            throw new ProfilerException("Unknown VmId " + vmId);
+        }
+
+        if (currentlyProfiledVmPids.contains((Integer) pid)) {
+            throw new ProfilerException("Already profiling the VM");
+        }
+
+        // TODO make this adjustable at run-time
+        // eg: asmJarPath + ":" + agentJarPath;
+        String jarsToLoad = "";
+        logger.info("Asking " + pid + " to load agent '" + agentJarPath + "' with arguments '" + jarsToLoad + "'");
+        // FIXME Only load the first time
+        remote.loadAgentIntoPid(pid, agentJarPath, jarsToLoad);
+
+        remote.startProfiling(pid);
+
+        currentlyProfiledVmPids.add(pid);
+        dao.addStatus(new ProfileStatusChange(agentId, vmId, clock.getRealTimeMillis(), true));
+    }
+
+    public synchronized void stopProfiling(String vmId) throws ProfilerException {
+        stopProfiling(vmId, true);
+    }
+
+    private void stopProfiling(String vmId, boolean alive) throws ProfilerException {
+        int pid = vmIdToPid.getPid(vmId);
+        if (pid == VmIdToPidMapper.UNKNOWN_VMID) {
+            throw new ProfilerException("VmId not found: " + vmId);
+        }
+
+        if (!currentlyProfiledVmPids.contains(pid)) {
+            throw new ProfilerException("Vm is not being profiled: " + vmId);
+        }
 
-        String profilingDataFile = (String) getInstrumentationAttribute(pid, "ProfilingDataFile");
+        ProfileUploader uploader = uploaderCreator.create(dao, agentId, vmId, pid);
+        if (alive) {
+            stopRemoteProfilerAndUploadResults(pid, uploader);
+        } else {
+            findAndUploadProfilingResultsStoredOnDisk(pid, uploader);
+        }
+        dao.addStatus(new ProfileStatusChange(agentId, vmId, clock.getRealTimeMillis(), false));
+        currentlyProfiledVmPids.remove((Integer) pid);
+    }
+
+    private void stopRemoteProfilerAndUploadResults(int pid, ProfileUploader uploader) throws ProfilerException {
+        remote.stopProfiling(pid);
+
+        String profilingDataFile = remote.getProfilingDataFile(pid);
+        upload(uploader, clock.getRealTimeMillis(), new File(profilingDataFile));
+    }
+
+    private void findAndUploadProfilingResultsStoredOnDisk(final int pid, ProfileUploader uploader) throws ProfilerException {
+        long timeStamp = clock.getRealTimeMillis();
+        // look for latest profiling data that it might have emitted on shutdown
+        File file = findProfilingResultFile(pid);
+        upload(uploader, timeStamp, file);
+    }
+
+    private File findProfilingResultFile(final int pid) {
+        // from InstrumentationControl:
+        // return Files.createTempFile("thermostat-" + getProcessId() + "-", ".perfdata", attributes);
+        String tmpDir = System.getProperty("java.io.tmpdir");
+        File[] files = new File(tmpDir).listFiles(new FilenameFilter() {
+            @Override
+            public boolean accept(File dir, String name) {
+                return name.startsWith("thermostat-" + pid + "-") && name.endsWith(".perfdata");
+            }
+        });
+
+        List<File> filesSortedByTimeStamp = Arrays.asList(files);
+        Collections.sort(filesSortedByTimeStamp, new MostRecentFileFirst());
+        return filesSortedByTimeStamp.get(0);
+    }
+
+    private void upload(ProfileUploader uploader, long timeStamp, File file) throws ProfilerException {
         try {
-            uploader.upload(clock.getRealTimeMillis(), new File(profilingDataFile));
+            uploader.upload(clock.getRealTimeMillis(), file);
         } catch (IOException e) {
             throw new ProfilerException("Unable to save profiling data into storage", e);
         }
     }
 
-    private Object invokeMethodOnInstrumentation(int pid, String name) throws ProfilerException {
-        try {
-            MXBeanConnection connection = connectionPool.acquire(pid);
-            try {
-                ObjectName instrumentation = new ObjectName("com.redhat.thermostat:type=InstrumentationControl");
-                MBeanServerConnection server = connection.get();
-                return server.invoke(instrumentation, name, new Object[0], new String[0]);
-            } finally {
-                connectionPool.release(pid, connection);
-            }
-        } catch (Exception e) {
-            throw new ProfilerException("Unable to communicate with remote profiler", e);
-        }
-    }
-
-    private Object getInstrumentationAttribute(int pid, String name) throws ProfilerException {
-        try {
-            MXBeanConnection connection = connectionPool.acquire(pid);
-            try {
-                ObjectName instrumentation = new ObjectName("com.redhat.thermostat:type=InstrumentationControl");
-                MBeanServerConnection server = connection.get();
-                return server.getAttribute(instrumentation, name);
-            } finally {
-                connectionPool.release(pid, connection);
-            }
-        } catch (Exception e) {
-            throw new ProfilerException("Unable to communicate with remote profiler", e);
-        }
-    }
-
-    static class Attacher {
-        VirtualMachine attach(String pid) throws AttachNotSupportedException, IOException {
-            return VirtualMachine.attach(pid);
-        }
-    }
 }
--- a/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ActivatorTest.java	Wed Dec 03 16:01:36 2014 +0100
+++ b/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ActivatorTest.java	Wed Dec 03 11:53:17 2014 -0500
@@ -42,6 +42,7 @@
 
 import org.junit.Test;
 
+import com.redhat.thermostat.agent.VmStatusListener;
 import com.redhat.thermostat.agent.command.RequestReceiver;
 import com.redhat.thermostat.agent.utils.management.MXBeanConnectionPool;
 import com.redhat.thermostat.storage.core.WriterID;
@@ -65,10 +66,12 @@
 
         activator.start(context);
 
-        assertTrue(context.isServiceRegistered(RequestReceiver.class.getName(), ProfileVmRequestReceiver.class));
+        assertTrue(context.isServiceRegistered(RequestReceiver.class.getName(), ProfilerRequestReceiver.class));
+        assertTrue(context.isServiceRegistered(VmStatusListener.class.getName(), ProfilerVmStatusListener.class));
 
         activator.stop(context);
 
-        assertFalse(context.isServiceRegistered(RequestReceiver.class.getName(), ProfileVmRequestReceiver.class));
+        assertFalse(context.isServiceRegistered(RequestReceiver.class.getName(), ProfilerRequestReceiver.class));
+        assertFalse(context.isServiceRegistered(VmStatusListener.class.getName(), ProfilerVmStatusListener.class));
     }
 }
--- a/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfileVmRequestReceiverTest.java	Wed Dec 03 16:01:36 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,168 +0,0 @@
-/*
- * Copyright 2012-2014 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.internal;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isA;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-
-import org.junit.Before;
-import org.junit.Test;
-
-import com.redhat.thermostat.agent.VmStatusListener.Status;
-import com.redhat.thermostat.common.Clock;
-import com.redhat.thermostat.common.command.Request;
-import com.redhat.thermostat.common.command.Response;
-import com.redhat.thermostat.common.command.Response.ResponseType;
-import com.redhat.thermostat.vm.profiler.agent.internal.ProfileVmRequestReceiver.ProfileUploaderCreator;
-import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
-import com.redhat.thermostat.vm.profiler.common.ProfileRequest;
-import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
-
-public class ProfileVmRequestReceiverTest {
-
-    private static final String AGENT_ID = "agent-id";
-
-    private static final String VM_ID = "foo";
-    private static final int VM_PID = 1;
-    private static final long TIMESTAMP = 99;
-
-    private ProfileVmRequestReceiver requestReceiver;
-
-    private Clock clock;
-    private VmProfiler profiler;
-    private ProfileDAO dao;
-    private ProfileUploader uploader;
-
-    @Before
-    public void setUp() {
-        clock = mock(Clock.class);
-        when(clock.getRealTimeMillis()).thenReturn(TIMESTAMP);
-
-        profiler = mock(VmProfiler.class);
-        dao = mock(ProfileDAO.class);
-        uploader = mock(ProfileUploader.class);
-        ProfileUploaderCreator uploaderCreator = mock(ProfileUploaderCreator.class);
-        when(uploaderCreator.create(dao, AGENT_ID, VM_ID, VM_PID)).thenReturn(uploader);
-
-        requestReceiver = new ProfileVmRequestReceiver(AGENT_ID, clock, profiler, dao, uploaderCreator);
-    }
-
-    @Test
-    public void doesNotProfileUnknownVm() {
-        Request request = ProfileRequest.create(null, VM_ID, ProfileRequest.START_PROFILING);
-        Response result = requestReceiver.receive(request);
-        assertEquals(ResponseType.NOK, result.getType());
-    }
-
-    @Test
-    public void profilesKnownVms() throws ProfilerException {
-        Request request = ProfileRequest.create(null, VM_ID, ProfileRequest.START_PROFILING);
-
-        requestReceiver.vmStatusChanged(Status.VM_STARTED, VM_ID, VM_PID);
-        Response result = requestReceiver.receive(request);
-
-        assertEquals(ResponseType.OK, result.getType());
-        verify(profiler).startProfiling(VM_PID);
-
-        verify(dao).addStatus(new ProfileStatusChange(AGENT_ID, VM_ID, TIMESTAMP, true));
-    }
-
-    @Test
-    public void startAndStopProfiling() throws ProfilerException {
-        Request request;
-
-        requestReceiver.vmStatusChanged(Status.VM_STARTED, VM_ID, VM_PID);
-        request = ProfileRequest.create(null, VM_ID, ProfileRequest.START_PROFILING);
-        Response result = requestReceiver.receive(request);
-
-        assertEquals(ResponseType.OK, result.getType());
-        verify(profiler).startProfiling(VM_PID);
-
-        request = ProfileRequest.create(null, VM_ID, ProfileRequest.STOP_PROFILING);
-        result = requestReceiver.receive(request);
-
-        assertEquals(ResponseType.OK, result.getType());
-        verify(profiler).stopProfiling(eq(VM_PID), isA(ProfileUploader.class));
-        verify(dao, times(2)).addStatus(isA(ProfileStatusChange.class));
-    }
-
-    @Test
-    public void doesNotProfileDeadVms() {
-        requestReceiver.vmStatusChanged(Status.VM_STARTED, VM_ID, VM_PID);
-        requestReceiver.vmStatusChanged(Status.VM_STOPPED, VM_ID, VM_PID);
-
-        Request request = ProfileRequest.create(null, VM_ID, ProfileRequest.START_PROFILING);
-        Response result = requestReceiver.receive(request);
-        assertEquals(ResponseType.NOK, result.getType());
-        verify(dao, never()).addStatus(isA(ProfileStatusChange.class));
-    }
-
-    @Test
-    public void readsProfilingResultsOnVmExit() throws Exception {
-        Request request;
-
-        requestReceiver.vmStatusChanged(Status.VM_STARTED, VM_ID, VM_PID);
-        request = ProfileRequest.create(null, VM_ID, ProfileRequest.START_PROFILING);
-        requestReceiver.receive(request);
-
-        // simulate target vm exiting
-        File profilingResults = new File(System.getProperty("java.io.tmpdir"), "thermostat-" + VM_PID + "-foobar.perfdata");
-        try (BufferedWriter writer = Files.newBufferedWriter(profilingResults.toPath(), StandardCharsets.UTF_8)) {
-            writer.append("test file, please ignore");
-        }
-
-        requestReceiver.vmStatusChanged(Status.VM_STOPPED, VM_ID, VM_PID);
-
-        verify(profiler, never()).stopProfiling(anyInt(), isA(ProfileUploader.class));
-        verify(uploader).upload(TIMESTAMP, profilingResults);
-        verify(dao, times(2)).addStatus(isA(ProfileStatusChange.class));
-
-        profilingResults.delete();
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerRequestReceiverTest.java	Wed Dec 03 11:53:17 2014 -0500
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2012-2014 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.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.Response;
+import com.redhat.thermostat.common.command.Response.ResponseType;
+import com.redhat.thermostat.vm.profiler.common.ProfileRequest;
+
+public class ProfilerRequestReceiverTest {
+
+    private static final String VM_ID = "foo";
+
+    private ProfilerRequestReceiver requestReceiver;
+
+    private VmProfiler profiler;
+
+    @Before
+    public void setUp() {
+        profiler = mock(VmProfiler.class);
+
+        requestReceiver = new ProfilerRequestReceiver(profiler);
+    }
+
+    @Test
+    public void forwardsStartRequestToProfiler() throws Exception {
+        Request request = ProfileRequest.create(null, VM_ID, ProfileRequest.START_PROFILING);
+        Response result = requestReceiver.receive(request);
+
+        verify(profiler).startProfiling(VM_ID);
+
+        assertEquals(ResponseType.OK, result.getType());
+    }
+
+    @Test
+    public void exceptionThrownFromProfilerResultsInStarError() throws Exception {
+        doThrow(ProfilerException.class).when(profiler).startProfiling(VM_ID);
+        Request request = ProfileRequest.create(null, VM_ID, ProfileRequest.START_PROFILING);
+        Response result = requestReceiver.receive(request);
+
+        assertEquals(ResponseType.NOK, result.getType());
+    }
+
+    @Test
+    public void forwardsStopRequestToProfiler() throws Exception {
+        Request request = ProfileRequest.create(null, VM_ID, ProfileRequest.STOP_PROFILING);
+        Response result = requestReceiver.receive(request);
+
+        verify(profiler).stopProfiling(VM_ID);
+
+        assertEquals(ResponseType.OK, result.getType());
+    }
+
+    @Test
+    public void exceptionThrownFromProfilerResultsInStopError() throws Exception {
+        doThrow(ProfilerException.class).when(profiler).stopProfiling(VM_ID);
+        Request request = ProfileRequest.create(null, VM_ID, ProfileRequest.STOP_PROFILING);
+        Response result = requestReceiver.receive(request);
+
+        assertEquals(ResponseType.NOK, result.getType());
+    }
+
+    @Test
+    public void unknownRequestCausesError() {
+        Request request = ProfileRequest.create(null, VM_ID, "bad-request");
+        Response result = requestReceiver.receive(request);
+
+        verifyNoMoreInteractions(profiler);
+
+        assertEquals(ResponseType.NOK, result.getType());
+    }
+
+    @Test
+    public void receiverIsValidTargetForRequest() throws Exception {
+        Request request = ProfileRequest.create(null, VM_ID, ProfileRequest.START_PROFILING);
+        assertEquals(ProfilerRequestReceiver.class.getName(), request.getReceiver());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/ProfilerVmStatusListenerTest.java	Wed Dec 03 11:53:17 2014 -0500
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2012-2014 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.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.agent.VmStatusListener.Status;
+
+
+public class ProfilerVmStatusListenerTest {
+
+    private static final String VM_ID = "foo";
+    private static final int VM_PID = 1;
+
+    private VmProfiler profiler;
+
+    private ProfilerVmStatusListener listener;
+
+    @Before
+    public void setUp() {
+        profiler = mock(VmProfiler.class);
+        listener = new ProfilerVmStatusListener(profiler);
+    }
+
+    @Test
+    public void forwardsStartEventToProfiler() throws Exception {
+        listener.vmStatusChanged(Status.VM_STARTED, VM_ID, VM_PID);
+        verify(profiler).vmStarted(VM_ID, VM_PID);
+    }
+
+    @Test
+    public void forwardsActiveEventToProfiler() throws Exception {
+        listener.vmStatusChanged(Status.VM_ACTIVE, VM_ID, VM_PID);
+        verify(profiler).vmStarted(VM_ID, VM_PID);
+    }
+
+    @Test
+    public void forwardsStopEventToProfiler() throws Exception {
+        listener.vmStatusChanged(Status.VM_STOPPED, VM_ID, VM_PID);
+        verify(profiler).vmStopped(VM_ID, VM_PID);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/RemoteProfilerCommunicatorTest.java	Wed Dec 03 11:53:17 2014 -0500
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2012-2014 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.internal;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import javax.management.MBeanServerConnection;
+import javax.management.ObjectName;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.agent.utils.management.MXBeanConnection;
+import com.redhat.thermostat.agent.utils.management.MXBeanConnectionPool;
+import com.redhat.thermostat.vm.profiler.agent.internal.RemoteProfilerCommunicator.Attacher;
+import com.sun.tools.attach.VirtualMachine;
+
+public class RemoteProfilerCommunicatorTest {
+
+    private static final String OBJECT_NAME = "com.redhat.thermostat:type=InstrumentationControl";
+    private static final int PID = 0;
+
+    private MXBeanConnection connection;
+    private MXBeanConnectionPool pool;
+
+    private VirtualMachine vm;
+    private Attacher attacher;
+
+    private RemoteProfilerCommunicator communicator;
+
+    private MBeanServerConnection server;
+
+    @Before
+    public void setUp() throws Exception {
+        server = mock(MBeanServerConnection.class);
+        connection = mock(MXBeanConnection.class);
+        when(connection.get()).thenReturn(server);
+
+        pool = mock(MXBeanConnectionPool.class);
+        when(pool.acquire(PID)).thenReturn(connection);
+
+        vm = mock(VirtualMachine.class);
+        attacher = mock(Attacher.class);
+        when(attacher.attach(eq(String.valueOf(PID)))).thenReturn(vm);
+
+        communicator = new RemoteProfilerCommunicator(pool, attacher);
+    }
+
+    @Test
+    public void loadAgentIntoPidAttachesAndLoadsTheAgent() throws Exception {
+        String AGENT_JAR = "agent-jar";
+        String AGENT_OPTIONS = "foo-bar";
+
+        communicator.loadAgentIntoPid(PID, AGENT_JAR, AGENT_OPTIONS);
+
+        verify(attacher).attach(String.valueOf(PID));
+        verify(vm).loadAgent(AGENT_JAR, AGENT_OPTIONS);
+        verify(vm).detach();
+        verifyNoMoreInteractions(vm);
+    }
+
+    @Test
+    public void startProfilingMakesAnRmiCall() throws Exception {
+        communicator.startProfiling(PID);
+
+        verify(server).invoke(
+                new ObjectName(OBJECT_NAME),
+                "startProfiling",
+                new Object[0],
+                new String[0]);
+        verifyNoMoreInteractions(server);
+        verify(pool).release(PID, connection);
+    }
+
+    @Test
+    public void stopProfilingMakesAnRmiCall() throws Exception {
+        communicator.stopProfiling(PID);
+
+        verify(server).invoke(
+                new ObjectName(OBJECT_NAME),
+                "stopProfiling",
+                new Object[0],
+                new String[0]);
+        verify(pool).release(PID, connection);
+    }
+
+    @Test
+    public void getDataFileWorks() throws Exception {
+        communicator.getProfilingDataFile(PID);
+
+        verify(server).getAttribute(
+                new ObjectName(OBJECT_NAME),
+                "ProfilingDataFile");
+        verify(pool).release(PID, connection);
+    }
+}
--- a/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/VmProfilerTest.java	Wed Dec 03 16:01:36 2014 +0100
+++ b/vm-profiler/agent/src/test/java/com/redhat/thermostat/vm/profiler/agent/internal/VmProfilerTest.java	Wed Dec 03 11:53:17 2014 -0500
@@ -36,101 +36,150 @@
 
 package com.redhat.thermostat.vm.profiler.agent.internal;
 
-import static org.mockito.Matchers.isA;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import java.io.BufferedWriter;
 import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.Properties;
 
-import javax.management.MBeanServerConnection;
-import javax.management.ObjectName;
-
 import org.junit.Before;
 import org.junit.Test;
 
-import com.redhat.thermostat.agent.utils.management.MXBeanConnection;
-import com.redhat.thermostat.agent.utils.management.MXBeanConnectionPool;
 import com.redhat.thermostat.common.Clock;
-import com.redhat.thermostat.vm.profiler.agent.internal.VmProfiler.Attacher;
-import com.sun.tools.attach.VirtualMachine;
+import com.redhat.thermostat.vm.profiler.agent.internal.VmProfiler.ProfileUploaderCreator;
+import com.redhat.thermostat.vm.profiler.common.ProfileDAO;
+import com.redhat.thermostat.vm.profiler.common.ProfileStatusChange;
 
 public class VmProfilerTest {
 
+    private static final String AGENT_ID = "some-agent";
+    private static final String VM_ID = "some-vm";
+    private static final int PID = 0;
+
+    private static final String AGENT_JAR = "foo";
+    private static final String ASM_JAR = "bar";
+    private static final long TIMESTAMP = 1_000_000_000;
+
     private VmProfiler profiler;
 
-    private VirtualMachine vm;
-    private MBeanServerConnection server;
-    private MXBeanConnection connection;
-    private MXBeanConnectionPool connectionPool;
-    private Attacher attacher;
+    private RemoteProfilerCommunicator remote;
     private Clock clock;
+    private ProfileDAO dao;
 
-    private final int PID = 0;
-
-    private final String AGENT_JAR = "foo";
-    private final String ASM_JAR = "bar";
-    private final long TIME = 1_000_000_000;
-
-    private ObjectName instrumentationName;
+    private ProfileUploader uploader;
+    private ProfileUploaderCreator profileUploaderCreator;
 
     @Before
     public void setUp() throws Exception {
-        instrumentationName = new ObjectName("com.redhat.thermostat:type=InstrumentationControl");
-
         Properties props = new Properties();
         props.setProperty("AGENT_JAR", AGENT_JAR);
         props.setProperty("ASM_JAR", ASM_JAR);
 
+        dao = mock(ProfileDAO.class);
+
         clock = mock(Clock.class);
-        when(clock.getRealTimeMillis()).thenReturn(TIME);
+        when(clock.getRealTimeMillis()).thenReturn(TIMESTAMP);
 
-        attacher = mock(Attacher.class);
-        vm = mock(VirtualMachine.class);
-        when(attacher.attach(isA(String.class))).thenReturn(vm);
+        uploader = mock(ProfileUploader.class);
+        profileUploaderCreator = mock(ProfileUploaderCreator.class);
+        when(profileUploaderCreator.create(dao, AGENT_ID, VM_ID, PID)).thenReturn(uploader);
+
+        remote = mock(RemoteProfilerCommunicator.class);
+
+        profiler = new VmProfiler(AGENT_ID, props, dao, clock, profileUploaderCreator, remote);
+    }
 
-        server = mock(MBeanServerConnection.class);
-
-        connection = mock(MXBeanConnection.class);
-        when(connection.get()).thenReturn(server);
+    @Test (expected=ProfilerException.class)
+    public void doesNotProfileNotStartedVms() throws Exception {
+        profiler.startProfiling(VM_ID);
+    }
 
-        connectionPool = mock(MXBeanConnectionPool.class);
-        when(connectionPool.acquire(PID)).thenReturn(connection);
+    @Test (expected=ProfilerException.class)
+    public void doesNotProfileDeadVms() throws Exception {
+        profiler.vmStarted(VM_ID, PID);
+        profiler.vmStopped(VM_ID, PID);
+        profiler.startProfiling(VM_ID);
+    }
 
-        profiler = new VmProfiler(props, connectionPool, attacher, clock);
+    @Test (expected=ProfilerException.class)
+    public void doesNotStartingProfilingTwice() throws Exception {
+        profiler.vmStarted(VM_ID, PID);
+        profiler.startProfiling(VM_ID);
+        profiler.startProfiling(VM_ID);
     }
 
     @Test
     public void startingProfilingLoadsJvmAgentAndMakesAnRmiCall() throws Exception {
-        profiler.startProfiling(PID);
+        profiler.vmStarted(VM_ID, PID);
+        profiler.startProfiling(VM_ID);
+
+        verify(remote).loadAgentIntoPid(PID, AGENT_JAR, "");
+        verify(remote).startProfiling(PID);
+        verify(dao).addStatus(new ProfileStatusChange(AGENT_ID, VM_ID, TIMESTAMP, true));
+        verifyNoMoreInteractions(remote);
+    }
+
+    @Test (expected=ProfilerException.class)
+    public void doesNotStopProfilingAnUnknownVm() throws Exception {
+        profiler.stopProfiling(VM_ID);
+    }
 
-        verify(attacher).attach(String.valueOf(PID));
-        verify(vm).loadAgent(AGENT_JAR, "");
-        verify(vm).detach();
-        verifyNoMoreInteractions(vm);
+    @Test (expected=ProfilerException.class)
+    public void doesNotStopProfilingNonProfiledVm() throws Exception {
+        profiler.vmStarted(VM_ID, PID);
+        profiler.stopProfiling(VM_ID);
+    }
 
-        verify(server).invoke(instrumentationName, "startProfiling", new Object[0], new String[0]);
-        verify(connectionPool).release(PID, connection);
+    @Test (expected=ProfilerException.class)
+    public void errorOnStoppingTwice() throws Exception {
+        final String FILE = "foobar";
+        when(remote.getProfilingDataFile(PID)).thenReturn(FILE);
+
+        profiler.vmStarted(VM_ID, PID);
+        profiler.startProfiling(VM_ID);
+        profiler.stopProfiling(VM_ID);
+        profiler.stopProfiling(VM_ID);
     }
 
     @Test
-    public void stoppingProfilingLoadsJvmAgentAndMakesAnRmiCall() throws Exception {
+    public void stoppingProfilingMakesInvokesStopUsingRmiAndUploadsData() throws Exception {
+
         final String FILE = "foobar";
-        when(server.getAttribute(instrumentationName, "ProfilingDataFile")).thenReturn(FILE);
+        when(remote.getProfilingDataFile(PID)).thenReturn(FILE);
 
-        ProfileUploader uploader = mock(ProfileUploader.class);
+        profiler.vmStarted(VM_ID, PID);
+        profiler.startProfiling(VM_ID);
+        profiler.stopProfiling(VM_ID);
 
-        profiler.stopProfiling(PID, uploader);
+        verify(dao).addStatus(new ProfileStatusChange(AGENT_ID, VM_ID, TIMESTAMP, false));
+
+        verify(remote).stopProfiling(PID);
+        verify(uploader).upload(TIMESTAMP, new File(FILE));
+        verifyNoMoreInteractions(uploader);
+    }
 
-        verifyNoMoreInteractions(vm);
+    @Test
+    public void gathersAndUploadsProfileDataOnVmExit() throws Exception {
+        // this is written on the target vm exit
+        File profilingResults = new File(System.getProperty("java.io.tmpdir"), "thermostat-" + PID + "-foobar.perfdata");
+        try (BufferedWriter writer = Files.newBufferedWriter(profilingResults.toPath(), StandardCharsets.UTF_8)) {
+            writer.append("test file, please ignore");
+        }
 
-        verify(server).invoke(instrumentationName, "stopProfiling", new Object[0], new String[0]);
-        verify(uploader).upload(TIME, new File(FILE));
-        verify(connectionPool, times(2)).release(PID, connection);
+        profiler.vmStarted(VM_ID, PID);
+        profiler.startProfiling(VM_ID);
+        profiler.vmStopped(VM_ID, PID);
 
+        verify(remote, never()).stopProfiling(PID);
+        verify(uploader).upload(TIMESTAMP, profilingResults);
+        verify(dao).addStatus(new ProfileStatusChange(AGENT_ID, VM_ID, TIMESTAMP, false));
 
+        profilingResults.delete();
     }
 }
--- a/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/ProfileRequest.java	Wed Dec 03 16:01:36 2014 +0100
+++ b/vm-profiler/common/src/main/java/com/redhat/thermostat/vm/profiler/common/ProfileRequest.java	Wed Dec 03 11:53:17 2014 -0500
@@ -45,7 +45,7 @@
 
     public static Request create(InetSocketAddress address, String vmId, String action) {
         Request request = new Request(RequestType.RESPONSE_EXPECTED, address);
-        request.setReceiver("com.redhat.thermostat.vm.profiler.agent.internal.ProfileVmRequestReceiver");
+        request.setReceiver("com.redhat.thermostat.vm.profiler.agent.internal.ProfilerRequestReceiver");
         request.setParameter(Request.ACTION, ProfileRequest.NAME);
         request.setParameter(ProfileRequest.PROFILE_ACTION, action);
         request.setParameter(ProfileRequest.VM_ID, vmId);