/*
 * Capsule
 * Copyright (c) 2014-2015, Parallel Universe Software Co. All rights reserved.
 * 
 * This program and the accompanying materials are licensed under the terms 
 * of the Eclipse Public License v1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package co.paralleluniverse.common;

import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

/**
 *
 * @author pron
 */
public final class ProcessUtil {
    /*
     * see https://weblogs.java.net/blog/emcmanus/archive/2007/08/combining_casca.html
     */
    private static volatile Field pidField;
    private static final String PROP_LOCAL_CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress";
    private static final String PROP_JAVA_HOME = "java.home";
    private static final Path MANAGEMENT_AGENT = Paths.get("lib", "management-agent.jar");

    /**
     * Returns the process ID on UNIX machines (fails on Windows).
     */
    public static int getPid(Process process) {
        if (!process.getClass().getName().equals("java.lang.UNIXProcess"))
            throw new UnsupportedOperationException("This operation is only supported in POSIX environments (Linux/Unix/MacOS");
        if (pidField == null) { // benign race
            try {
                Field f = process.getClass().getDeclaredField("pid");
                f.setAccessible(true);
                pidField = f;
            } catch (NoSuchFieldException e) {
                throw new AssertionError(e);
            } catch (SecurityException e) {
                throw new RuntimeException(e);
            }
        }

        try {
            return pidField.getInt(process);
        } catch (IllegalAccessException e) {
            throw new AssertionError(e);
        }
    }

    /**
     * Connects to a child JVM process
     *
     * @param p          the process to which to connect
     * @param startAgent whether to installed the JMX agent in the target process if not already in place
     * @return an {@link MBeanServerConnection} to the process's MBean server
     */
    public static MBeanServerConnection getMBeanServerConnection(Process p, boolean startAgent) {
        try {
            final JMXServiceURL serviceURL = getLocalConnectorAddress(p, startAgent);
            final JMXConnector connector = JMXConnectorFactory.connect(serviceURL);
            final MBeanServerConnection mbsc = connector.getMBeanServerConnection();
            return mbsc;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns the JMX connector address of a child process.
     *
     * @param p          the process to which to connect
     * @param startAgent whether to installed the JMX agent in the target process if not already in place
     * @return a {@link JMXServiceURL} to the process's MBean server
     */
    public static JMXServiceURL getLocalConnectorAddress(Process p, boolean startAgent) {
        return getLocalConnectorAddress(Integer.toString(getPid(p)), startAgent);
    }

    private static JMXServiceURL getLocalConnectorAddress(String id, boolean startAgent) {
        VirtualMachine vm = null;
        try {
            try {
                vm = VirtualMachine.attach(id);
                String connectorAddr = vm.getAgentProperties().getProperty(PROP_LOCAL_CONNECTOR_ADDRESS);
                if (connectorAddr == null && startAgent) {
                    final String agent = Paths.get(vm.getSystemProperties().getProperty(PROP_JAVA_HOME)).resolve(MANAGEMENT_AGENT).toString();
                    vm.loadAgent(agent);
                    connectorAddr = vm.getAgentProperties().getProperty(PROP_LOCAL_CONNECTOR_ADDRESS);
                }
                vm.detach();
                final JMXServiceURL url = connectorAddr != null ? new JMXServiceURL(connectorAddr) : null;
                return url;
            } catch (AttachNotSupportedException e) {
                throw new UnsupportedOperationException(e);
            }
        } catch (Throwable e) {
            try {
                if (vm != null)
                    vm.detach();
            } catch (IOException ex) {
                e.addSuppressed(ex);
            }
            throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
        }
    }

    private ProcessUtil() {
    }
}