/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.lib.profiler.server;

import org.netbeans.lib.profiler.global.CalibrationDataFileIO;
import org.netbeans.lib.profiler.global.CommonConstants;
import org.netbeans.lib.profiler.global.Platform;
import org.netbeans.lib.profiler.global.ProfilingSessionStatus;
import org.netbeans.lib.profiler.server.system.Classes;
import org.netbeans.lib.profiler.server.system.GC;
import org.netbeans.lib.profiler.server.system.HeapDump;
import org.netbeans.lib.profiler.server.system.ThreadDump;
import org.netbeans.lib.profiler.server.system.Threads;
import org.netbeans.lib.profiler.server.system.Timers;
import org.netbeans.lib.profiler.wireprotocol.*;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.*;
import java.text.MessageFormat;
import java.util.*;


/**
 * This class contains functionality for starting (attaching to) the Target Application (TA), and for
 * communication between the profiling back end and the tool (server and client).
 *
 * @author Tomas Hurka
 * @author Misha Dmitriev
 * @author Ian Formanek
 */
public class ProfilerServer extends Thread implements CommonConstants {
    //~ Inner Classes ------------------------------------------------------------------------------------------------------------

    private static class AttachDynamicThread extends Thread {
        //~ Instance fields ------------------------------------------------------------------------------------------------------

        private int activateCode;

        //~ Constructors ---------------------------------------------------------------------------------------------------------

        AttachDynamicThread(int activateCode) {
            this.setName(PROFILER_SPECIAL_EXEC_THREAD_NAME + " 5"); // NOI18N
            this.activateCode = activateCode;
        }

        //~ Methods --------------------------------------------------------------------------------------------------------------

        public void run() {
            try {
                doActivate(activateCode);
            } catch (Throwable ex) {
                System.err.println("Profiler dynamic attach initialization failed due to:"); //NOI18N
                ex.printStackTrace();
            }
        }
    }

    // Copied from org.openide.util.NbBundle
    // Does not support branding!
    private static class LocaleIterator extends Object implements Iterator {
        //~ Instance fields ------------------------------------------------------------------------------------------------------

        /**
         * current locale, and initial locale
         */
        private Locale initLocale;

        /**
         * current locale, and initial locale
         */
        private Locale locale;

        /**
         * the branding string in use
         */
        private String branding;

        /**
         * current suffix which will be returned in next calling nextElement
         */
        private String current;

        /**
         * this flag means, if default locale is in progress
         */
        private boolean defaultInProgress = false;

        /**
         * this flag means, if empty suffix was exported yet
         */
        private boolean empty = false;

        //~ Constructors ---------------------------------------------------------------------------------------------------------

        /**
         * Creates new LocaleIterator for given locale.
         *
         * @param locale given Locale
         */
        public LocaleIterator(Locale locale) {
            this.locale = this.initLocale = locale;

            if (locale.equals(Locale.getDefault())) {
                defaultInProgress = true;
            }

            current = '_' + locale.toString(); // NOI18N

            //            if (brandingToken == null) {
            branding = null;

            //            } else {
            //                branding = "_" + brandingToken; // NOI18N
            //            }

            //System.err.println("Constructed: " + this);
        }

        //~ Methods --------------------------------------------------------------------------------------------------------------

        /**
         * Tests if there is any suffix.
         */
        public boolean hasNext() {
            return (current != null);
        }

        /**
         * @return next suffix.
         * @throws NoSuchElementException if there is no more locale suffix.
         */
        public Object next() throws NoSuchElementException {
            if (current == null) {
                throw new NoSuchElementException();
            }

            final String ret;

            if (branding == null) {
                ret = current;
            } else {
                ret = branding + current;
            }

            int lastUnderbar = current.lastIndexOf('_'); // NOI18N

            if (lastUnderbar == 0) {
                if (empty) {
                    reset();
                } else {
                    current = ""; // NOI18N
                    empty = true;
                }
            } else {
                if (lastUnderbar == -1) {
                    if (defaultInProgress) {
                        reset();
                    } else {
                        // [PENDING] stuff with trying the default locale
                        // after the real one does not actually seem to work...
                        locale = Locale.getDefault();
                        current = '_' + locale.toString(); // NOI18N
                        defaultInProgress = true;
                    }
                } else {
                    current = current.substring(0, lastUnderbar);
                }
            }

            //System.err.println("Returning: `" + ret + "' from: " + this);
            return ret;
        }

        public void remove() throws UnsupportedOperationException {
            throw new UnsupportedOperationException();
        }

        /**
         * Finish a series.
         * If there was a branding prefix, restart without that prefix
         * (or with a shorter prefix); else finish.
         */
        private void reset() {
            if (branding != null) {
                current = '_' + initLocale.toString(); // NOI18N

                int idx = branding.lastIndexOf('_'); // NOI18N

                if (idx == 0) {
                    branding = null;
                } else {
                    branding = branding.substring(0, idx);
                }

                empty = false;
            } else {
                current = null;
            }
        }
    }

    /**
     * A shutdown wait thread
     */
    private static class ShutdownWaitThread extends Thread {
        //~ Constructors ---------------------------------------------------------------------------------------------------------

        public ShutdownWaitThread() {
            setName(PROFILER_SPECIAL_EXEC_THREAD_NAME + " 7"); // NOI18N
        }

        //~ Methods --------------------------------------------------------------------------------------------------------------

        public void run() {
            if (preemptExit && connectionOpen) {
                profilerServer.sendSimpleCmdToClient(Command.SHUTDOWN_INITIATED);
                waitForShutdownOK();
                cleanupOnShutdown();

                // ... and proceed with shutdown
            }
        }
    }

    /**
     * A thread to execute certain commands in (see comments to executeInSeparateThread above)
     */
    private class SeparateCmdExecutionThread extends Thread {
        //~ Instance fields ------------------------------------------------------------------------------------------------------

        private volatile boolean stopped = false;

        //~ Constructors ---------------------------------------------------------------------------------------------------------

        public SeparateCmdExecutionThread() {
            ThreadInfo.addProfilerServerThread(this);
            setName(PROFILER_SPECIAL_EXEC_THREAD_NAME + " 6"); // NOI18N
            setDaemon(true);
        }

        //~ Methods --------------------------------------------------------------------------------------------------------------

        public void run() {
            synchronized (execInSeparateThreadLock) {
                while (true) {
                    try {
                        execInSeparateThreadLock.wait();
                    } catch (InterruptedException ex) {
                        System.err.println(THREAD_WAIT_EXCEPTION_MSG);
                    }

                    if (stopped) {
                        ThreadInfo.removeProfilerServerThread(this);
                        return;
                    }

                    int opCode = execInSeparateThreadOpCode;

                    switch (opCode) {
                        case Command.DUMP_EXISTING_RESULTS:
                        case Command.DUMP_EXISTING_RESULTS_LIVE:

                            long absTimeStamp = ProfilerRuntimeCPU.getAbsTimeStampInCollectedFormat();
                            boolean res = false;

                            if ((ProfilerRuntime.eventBuffer != null) && !ProfilerRuntime.sendingBuffer) {
                                synchronized (ProfilerRuntime.eventBuffer) {
                                    res = ProfilerInterface.serialClientOperationsLock.beginTrans(true, true);

                                    if (res) {
                                        try {
                                            ProfilerInterface.dumpExistingResults(opCode == Command.DUMP_EXISTING_RESULTS_LIVE);
                                        } finally {
                                            ProfilerInterface.serialClientOperationsLock.endTrans();
                                        }
                                    }
                                }
                            }

                            DumpResultsResponse resp = new DumpResultsResponse(res, absTimeStamp);
                            sendComplexResponseToClient(resp);

                            break;
                        case Command.RESET_PROFILER_COLLECTORS:
                            requestClientResetResults();
                            sendSimpleResponseToClient(true, null);

                            break;
                    }
                }
            }
        }

        public void terminate() {
            stopped = true;
        }
    }

    //~ Static fields/initializers -----------------------------------------------------------------------------------------------

    // -----
    // I18N String constants
    // !!! Warning - do not use ResourceBundle.getBundle here, won't work in context of direct/dynamic attach !!!
    // Default EN messages initialized here, will be replaced by localized messages in initLocalizedResources()
    private static ResourceBundle messages;
    private static String ENTER_TO_SHUTDOWN_MSG = "Press ENTER to shut down the target JVM..."; // NOI18N
    private static String MAIN_CLASS_NOT_PUBLIC_MSG = "Main class {0} is not public.\nProfiler can not start it"; // NOI18N
    private static String INCORRECT_MAIN_MODIFIERS_MSG = "Method {0}.main(String args[]) has incorrect modifiers"; // NOI18N
    private static String UNEXPECTED_EXCEPTION_MSG = "Target application threw an unexpected exception: {0}"; // NOI18N
    private static String ELAPSED_TIME_MSG = "Main application thread elapsed time: {0} ms."; // NOI18N
    private static String CONNECTION_MSG = "Profiler Agent: Established connection with the tool"; // NOI18N
    private static String WAITING_ON_PORT_MSG = "Profiler Agent: Waiting for connection on port {0} (Protocol version: {1})"; // NOI18N
    private static String WAITING_ON_PORT_TIMEOUT_MSG = "Profiler Agent: Waiting for connection on port {0}, timeout {1} seconds (Protocol version: {2})"; // NOI18N
    private static String CONNECTION_EXCEPTION_MSG = "Profiler Agent Error: Exception when trying to establish connection with client:\n{0}"; // NOI18N
    private static String CONNECTION_EXCEPTION_BIND_MSG = "Profiler Agent Error: Make sure the previously profiled process has been fully terminated"; // NOI18N
    private static String CONNECTION_TIMEOUT_MSG = "Profiler Agent Error: Timed out trying to establish connection with client"; // NOI18N
    private static String AGENT_ERROR_MSG = "Profiler Agent Error: {0}"; // NOI18N
    private static String CONNECTION_INTERRUPTED_MSG = "Profiler Agent Error: Connection with client interrupted"; // NOI18N
    private static String COMMAND_EXCEPTION_MSG = "Profiler Agent Error: Exception when handling command from client:\n{0}"; // NOI18N
    private static String RESPONSE_EXCEPTION_MSG = "Profiler Agent Error: Exception when trying to send response or command to client:\n{0}"; // NOI18N
    private static String CONNECTION_CLOSED_MSG = "Profiler Agent: Connection with agent closed"; // NOI18N
    private static String CONNECTION_CLOSED_EX_MSG = "Profiler Agent: Connection with agent closed (ProfilingSessionStatus was null)"; // NOI18N
    private static String INCORRECT_AGENT_ID_MSG = "Profiler Agent Warning: Wrong agentId specified: {0}"; // NOI18N
    private static String THREAD_EXCEPTION_MSG = "Profiler Agent Error: Exception in executeInSeparateThread()"; // NOI18N
    private static String THREAD_WAIT_EXCEPTION_MSG = "Profiler Agent Error: Exception in wait in SeparateCmdExecutionThread"; // NOI18N
    private static String LOCAL_SESSION_MSG = "Profiler Agent: Local accelerated session"; // NOI18N
    private static String REMOTE_SESSION_MSG = "Profiler Agent: Standard session"; // NOI18N
    
    public static final int ATTACH_DYNAMIC = 0;
    public static final int ATTACH_DIRECT = 1;
    private static volatile boolean profilerInterfaceInitialized;
    private static volatile boolean connectionOpen;
    private static volatile boolean connectionFailed;
    private static volatile boolean detachCommandReceived;
    private static ProfilerServer profilerServer;
    private static ProfilingSessionStatus status;
    private static volatile boolean startTargetApp;
    private static volatile boolean targetAppMainThreadComplete;
    private static volatile Exception startupException;
    private static final Object targetAppRunningLock = new Object();
    private static Thread mainThread;

    // Management of execution of some commands in a separate thread
    private static SeparateCmdExecutionThread separateCmdExecutionThread;
    private static ShutdownWaitThread shutdownWaitThread;
    static final Object execInSeparateThreadLock = new Object();
    static int execInSeparateThreadOpCode;
    private static volatile boolean preemptExit = true;
    private static boolean shutdownOK = false;
    private static final Object shutdownLock = new Object();
    private static final Object resultsNotifiedLock = new Object();

    // @GuardedBy resultsNotifiedLock
    private static boolean resultsNotified = false;
    private static boolean resourcesInitialized = false;

    // This data is needed to avoid passing parameters to doActivate() which may cause problems in attach by pid mode on Windows.
    private static String _fullJFluidPath;
    private static int _portNo;
    private static int _activateCode;
    private static int _timeOut = 0;
    private static Response lastResponse;
    private static final Object responseLock = new Object();

    //~ Instance fields ----------------------------------------------------------------------------------------------------------

    private ObjectInputStream socketIn;
    private ObjectOutputStream socketOut;
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private WireIO wireIO;
    private boolean dynamic;
    private int agentId = -1;
    private final Random r = new Random(System.currentTimeMillis()) ;

    //---------------------------------------------------------------------------------------
    // Communication management
    //---------------------------------------------------------------------------------------
    private int serverPort;
    private int serverTimeout = 0; // no timeout by default

    //~ Constructors -------------------------------------------------------------------------------------------------------------

    private ProfilerServer(int port, boolean dynamic, int timeout) {
        super(PROFILER_SERVER_THREAD_NAME);
        setPriority(Thread.MAX_PRIORITY);
        serverPort = port;
        ThreadInfo.addProfilerServerThread(this);
        this.dynamic = dynamic;

        if (!dynamic) {
            // for dynamic attach, the server should never timeout
            serverTimeout = timeout;
        }

        setDaemon(true);
    }

    //~ Methods ------------------------------------------------------------------------------------------------------------------

    public static synchronized Response getLastResponse() {
        Response res;

        synchronized (responseLock) {
            if (lastResponse == null) {
                // I had to introduce the check below, since for some applications, seemingly the GUI ones that open a FileChooser dialog,
                // we can somehow get an InterruptedException below. This is likely a bug in JDK - maybe AWT just browses and calls
                // Thread.interrupt() that causes this exception, on waiting threads, and can mistake our thread for its own or something.
                boolean gotInterrupted = false;

                do {
                    try {
                        responseLock.wait();
                        gotInterrupted = false;
                    } catch (InterruptedException ex) {
                        //System.err.println("*** JFluid warning: InterruptedException in ProfilerServer.getLastResponse()");
                        gotInterrupted = true;
                    }
                } while (gotInterrupted);

                if (lastResponse == null) {
                    System.out.println("Profiler Agent Error: lastResponse == null - internal error?"); // NOI18N
                }
            }

            res = lastResponse;
            lastResponse = null;
        }

        return res;
    }

    public static Thread getMainThread() {
        return mainThread;
    }

    public static ProfilingSessionStatus getProfilingSessionStatus() {
        return status;
    }

    public static boolean isTargetAppMainThreadComplete() {
        return targetAppMainThreadComplete;
    }

    public static void activate(String fullJFluidPath, int portNo, final int activateCode) {
        activate(fullJFluidPath, portNo, activateCode, 0);
    }

    /**
     * Entrypoint in the usage scenario when the client attaches to the running target app using an OS signal,
     * or the "attach on startup" method. On JDK 1.5,
     * called from ProfilerActivate15.premain().
     * activateCode == 0 : "attach on the fly", activateCode == 1 : "attach on startup"
     *
     * @param fullJFluidPath Full path to the agent libs
     * @param portNo         Port number to use
     * @param activateCode   one of ATTACH_DIRECT or ATTACH_DYNAMIC, determines whether the server is started in dynamic
     *                       attach mode ( JDK 1.6) or Direct attach
     * @param timeOut        Time out in seconds for server socket, or 0 for no timeout
     * @see #ATTACH_DIRECT
     * @see #ATTACH_DYNAMIC
     */
    public static void activate(String fullJFluidPath, int portNo, final int activateCode, int timeOut) {
        if (connectionOpen) {
            throw new IllegalStateException("Connection already opened on port "+portNo);   // NOI18N
        }
        try {
            _fullJFluidPath = fullJFluidPath;
            _portNo = portNo;
            _timeOut = timeOut;
            _activateCode = activateCode;

            initLocalizedResources();

            if (activateCode == ATTACH_DYNAMIC) {
                // Creation of the new thread is (hopefully) a temporary workaround to avoid the problem with stack
                // overflow or something else when we attach on Windows "by pid", i.e. using the CreateRemoteThread() call.
                new AttachDynamicThread(activateCode).start();
            } else {
                doActivate(activateCode);
            }
        } catch (Throwable ex) {
            System.err.println("Profiler initialization failed due to:"); //NOI18N
            ex.printStackTrace();
        }
    }

    /**
     * Entrypoint in the usage scenario where the client starts and stops the target application.
     * Start the communication thread and then the target application.
     * args[0] is the full path to the directory where JFluid native libraries are contained.
     * args[1] is the communication port number.
     * args[2] (optional) if it is a number, it is a timeout for the profiler server (in seconds) to wait until the
     * client connects
     * args[2 or 3] is the target app main class name; args[3 or 4..n] are its arguments.
     */
    public static void main(String[] args) {
        mainThread = Thread.currentThread();

        // Fix for Issue 69454 - cannot find path to Profiler libraries (http://www.netbeans.org/issues/show_bug.cgi?id=69454)
        // _fullJFluidPath is needed for lazy initializing localized messages, but it was originally set only by the activate() method
        // Now it has to be set also here for the I18N to work
        try {
            _fullJFluidPath = new File(args[0]).getParentFile().getParentFile().getParentFile().getAbsolutePath();
        } catch (Exception ex) {
            throw new RuntimeException("ProfilerServer: Unable to initialize ResourceBundle for ProfilerServer, cannot resolve library directory\n" // NOI18N
                                       + ex.getMessage());
        }

        initLocalizedResources();
        initInternals();

        // Get the port number
        int portNo = 0;

        try {
            portNo = Integer.parseInt(args[1]);
        } catch (NumberFormatException e) {
            internalError("illegal port number specified: " + args[1]); // NOI18N
        }

        int idx = 2;

        // Get the optional timeout number
        int timeout = 0;

        try {
            timeout = Integer.parseInt(args[2]);
            idx = 3;
        } catch (NumberFormatException e) {
            // timeout not specified (it is optional)
        }

        // Move the target app arguments into special array
        int len = args.length - (idx + 1);
        String[] targetAppArgs = new String[len];
        System.arraycopy(args, idx + 1, targetAppArgs, 0, len);

        // Start the communication thread and wait for it to establish connection with client
        profilerServer = new ProfilerServer(portNo, true, timeout);
        profilerServer.start();

        while (!(connectionOpen || connectionFailed)) {
            delay(50);
        }

        if (connectionFailed) {
            // prevent the console from dying without the user being able to see the error
            // pressEnterToShutDown();
            // no cleanup in this case, as there is no connection established
            preemptExit = false;
            System.exit(-1);
        }

        ProfilerInterface.setProfilerServer(profilerServer);

        initSupportingFunctionality(false);

        // Accept, or wait for, the client command to start the target app, and then start it.
        while (!startTargetApp) {
            delay(100);
        }

        runTargetApp(args[idx], targetAppArgs);
        targetAppMainThreadComplete = true;

        // If we haven't actually managed to start the app, notify the waiting communication thread.
        if (startupException != null) {
            synchronized (targetAppRunningLock) {
                targetAppRunningLock.notify();
            }
        }

        // Wait for some time in case the target app started some threads and then exited the main thread, while the
        // offspring threads have not yet fully initialized
        delay(300);

        // Now wait until all target app threads (excluding this, main one) terminate.
        while (Threads.targetAppThreadsExist()) {
            delay(300);
        }

        status.targetAppRunning = false;
        ProfilerInterface.disableProfilerHooks(); // So that e.g. System.exit() doesn't cause class loads and command sends
                                                  // DEBUGGING: if it's needed to check how good is the sampling interval when sampled instrumentation is used,
                                                  // decomment the one below to make the sampling thread stop here and report the debug data.
                                                  // ProfilerRuntimeCPUSampledInstr.clearDataStructures();

        profilerServer.sendSimpleCmdToClient(Command.SHUTDOWN_INITIATED);
        waitForShutdownOK();
        forcedShutdown();
    }

    public void setRemoteProfiling(boolean remote) {
        status.remoteProfiling = remote;
        if (remote) {
            System.out.println(REMOTE_SESSION_MSG);
            ClassBytesLoader.preloadClasses(remote);
        } else {
            System.out.println(LOCAL_SESSION_MSG);
            if (Platform.getJDKVersionNumber() >= JDK_19) {
                // This is to preload some classes that can otherwise be loaded at inappropriate time and cause class load hook firing.
                ClassBytesLoader.preloadClasses(remote);
            }
        }
    }

    public static void notifyClientOnResultsAvailability() {
        if (!connectionOpen) {
            return;
        }

        if (profilerServer == null) {
            return; // in calibration mode
        }

        synchronized (resultsNotifiedLock) {
            if (resultsNotified) {
                return; // no need to notify again
            }

            resultsNotified = true;
            profilerServer.sendSimpleCmdToClient(Command.RESULTS_AVAILABLE);
        }
    }

    /**
     * reset data, including the data in event buffer
     */
    public static void requestClientResetResults() {
        // make sure resultsNotified flag is set to false
        profilerServer.resetResultsNotifiedFlag();
        ProfilerInterface.resetProfilerCollectors();
        ProfilerCalibrator.resetInternalStatsCollectors();
    }

    public static void requestClientTakeSnapshot() {
        if (profilerServer == null) {
            return; // in calibration mode
        }

        profilerServer.sendSimpleCmdToClient(Command.TAKE_SNAPSHOT);
    }

    public boolean getAndCheckLastResponse() {
        Response resp = getLastResponse();

        return resp.isOK();
    }

    public void run() {
        if (connectToClient()) {
            while (!profilerInterfaceInitialized) {
                delay(50);
            }

            listenToClient();
        } else {
            preemptExit = false;
        }
        ThreadInfo.removeProfilerServerThread(this);
    }

    public void sendClassLoaderUnloadingCommand() {
        sendSimpleCmdToClient(Command.CLASS_LOADER_UNLOADING);
        getLastResponse();
    }

    public synchronized void sendComplexCmdToClient(Command cmd) {
        try {
            wireIO.sendComplexCommand(cmd);
        } catch (IOException ex) {
            if (!detachCommandReceived) {
                handleIOExceptionOnSend(ex);
            }
        }
    }

    public synchronized void sendComplexResponseToClient(Response resp) {
        try {
            wireIO.sendComplexResponse(resp);
        } catch (IOException ex) {
            if (!detachCommandReceived) {
                handleIOExceptionOnSend(ex);
            }
        }
    }

    // Several methods to send commands specific for modules that use wireprotocol just occasionally
    public boolean sendEventBufferDumpedCommand(int length, String bufferName) {
        EventBufferDumpedCommand cmd = new EventBufferDumpedCommand(length,bufferName);
        sendComplexCmdToClient(cmd);
        return getAndCheckLastResponse();
    }

    public boolean sendEventBufferDumpedCommand(int length, byte[] buffer, int startPos) {
        EventBufferDumpedCommand cmd = new EventBufferDumpedCommand(length,buffer,startPos);
        sendComplexCmdToClient(cmd);

        return getAndCheckLastResponse();
    }

    public synchronized void sendSimpleCmdToClient(int cmdType) {
        try {
            wireIO.sendSimpleCommand(cmdType);
        } catch (IOException ex) {
            if (!detachCommandReceived) {
                handleIOExceptionOnSend(ex);
            }
        }
    }

    public synchronized void sendSimpleResponseToClient(boolean val, String errorMessage) {
        try {
            wireIO.sendSimpleResponse(val, errorMessage);
        } catch (IOException ex) {
            if (!detachCommandReceived) {
                handleIOExceptionOnSend(ex);
            }
        }
    }

    // --- I18N Support ----------------------------------------------------------

    // This method is used for obtaining ResourceBundle from classes that can be
    // used by ProfilerServer in context of direct/dynamic attach.
    //
    // If path to profiler server libraries (.jar) is known, ResourceBundle is obtained
    // using custom classloader (solves problem with bootstrap classloader&dynamic attach)
    //
    // Does not support branding!
    static ResourceBundle getProfilerServerResourceBundle() {
        if (messages != null) {
            return messages;
        }

        // 1. try to get the ResourceBundle using custom classloader
        if (_fullJFluidPath != null) {
            try {
                messages = getProfilerServerResourceBundle(_fullJFluidPath);
            } catch (Exception e) {
                System.err.println("Profiler Server: Problem with customized initializing localized messages...\n" + e.getMessage()); // NOI18N
            }
        }

        // cannot find jfluid-server.jar or Bundle.properties not found

        if (messages != null) {
            return messages; // ResourceBundle successfuly loaded using custom classloader
        }

        // 2. try to get the ResourceBundle in standard way
        try {
            messages = ResourceBundle.getBundle("org.netbeans.lib.profiler.server.Bundle"); // NOI18N
        } catch (Exception e) {
            System.err.println("Profiler Server: Problem with default initializing localized messages...\n" + e.getMessage()); // NOI18N
        }

        return messages;
    }

    static void initLocalizedResources() {
        if (resourcesInitialized) {
            return;
        }

        messages = getProfilerServerResourceBundle();

        if (messages != null) {
            ENTER_TO_SHUTDOWN_MSG = messages.getString("ProfilerServer_EnterToShutdownMsg"); // NOI18N
            MAIN_CLASS_NOT_PUBLIC_MSG = messages.getString("ProfilerServer_MainClassNotPublicMsg"); // NOI18N
            INCORRECT_MAIN_MODIFIERS_MSG = messages.getString("ProfilerServer_IncorrectMainModifiersMsg"); // NOI18N
            UNEXPECTED_EXCEPTION_MSG = messages.getString("ProfilerServer_UnexpectedExceptionMsg"); // NOI18N
            ELAPSED_TIME_MSG = messages.getString("ProfilerServer_ElapsedTimeMsg"); // NOI18N
            CONNECTION_MSG = messages.getString("ProfilerServer_ConnectionMsg"); // NOI18N
            WAITING_ON_PORT_MSG = messages.getString("ProfilerServer_WaitingOnPortMsg"); // NOI18N
            WAITING_ON_PORT_TIMEOUT_MSG = messages.getString("ProfilerServer_WaitingOnPortTimeoutMsg"); // NOI18N
            CONNECTION_EXCEPTION_MSG = messages.getString("ProfilerServer_ConnectionExceptionMsg"); // NOI18N
            CONNECTION_EXCEPTION_BIND_MSG = messages.getString("ProfilerServer_ConnectionExceptionBindMsg"); // NOI18N
            CONNECTION_TIMEOUT_MSG = messages.getString("ProfilerServer_ConnectionTimeoutMsg"); // NOI18N
            AGENT_ERROR_MSG = messages.getString("ProfilerServer_AgentErrorMsg"); // NOI18N
            CONNECTION_INTERRUPTED_MSG = messages.getString("ProfilerServer_ConnectionInterruptedMsg"); // NOI18N
            COMMAND_EXCEPTION_MSG = messages.getString("ProfilerServer_CommandExceptionMsg"); // NOI18N
            RESPONSE_EXCEPTION_MSG = messages.getString("ProfilerServer_ResponseExceptionMsg"); // NOI18N
            CONNECTION_CLOSED_MSG = messages.getString("ProfilerServer_ConnectionClosedMsg"); // NOI18N
            CONNECTION_CLOSED_EX_MSG = messages.getString("ProfilerServer_ConnectionClosedExMsg"); // NOI18N
            INCORRECT_AGENT_ID_MSG = messages.getString("ProfilerServer_IncorrectAgentIdMsg"); // NOI18N
            THREAD_EXCEPTION_MSG = messages.getString("ProfilerServer_ThreadExceptionMsg"); // NOI18N
            THREAD_WAIT_EXCEPTION_MSG = messages.getString("ProfilerServer_ThreadWaitExceptionMsg"); // NOI18N
            LOCAL_SESSION_MSG = messages.getString("ProfilerServer_LocalSessionMsg"); // NOI18N
            REMOTE_SESSION_MSG = messages.getString("ProfilerServer_RemoteSessionMsg"); // NOI18N
            resourcesInitialized = true;
        }
    }

    public static void loadNativeLibrary(String fullJFluidPath, boolean fullPathToLibSpecified) {
        String libFullName = Platform.getAgentNativeLibFullName(fullJFluidPath, fullPathToLibSpecified, null, -1);
        System.load(libFullName);
    }

    static boolean startProfilingPointsActive() {
        if (status != null) {
            return status.startProfilingPointsActive;
        }

        return false;
    }

    private static File getInfoFile(int port) throws IOException {
        String dirName = Platform.getProfilerUserDir();

        return new File(dirName + File.separator + port);
    }

    private static void setShutdownOK() {
        synchronized (shutdownLock) {
            shutdownOK = true;
            shutdownLock.notifyAll();
        }
    }

    private static void cleanupOnShutdown() {
        Monitors.shutdown();
        ProfilerInterface.disableProfilerHooks();
        ProfilerInterface.disableProfiling();

        // Bugfix for 65947: Profiler blocks a finishing profiled application
        // The following connectionOpen = false is done just to prevent error message from listenToClient(). When the connection
        // is closed either by the client or here by closeConnection(), whoever is faster, listenToClient() waiting for input in socket
        // will get IOException.
        // Be careful with this! sendResponseToClient() currently doesn't check connectionOpen value, but if it does, this should be changed.
        connectionOpen = false;
        profilerServer.sendSimpleCmdToClient(Command.SHUTDOWN_COMPLETED);
        profilerServer.closeConnection();
        profilerServer.stopSeparateCmdExecutionThread();
    }

    private static void delay(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
        }
    }

    /**
     * Note that putting the code of this into the custom thread above and thus executing in "attach and startup"
     * in a separate thread as well, causes the VM to crash. Probably a new thread can't be created in a call from
     * pre-main function.
     *
     * @param activateCode ATTACH_DYNAMIC or ATTACH_DIRECT
     * @see #ATTACH_DYNAMIC
     * @see #ATTACH_DIRECT
     */
    private static void doActivate(int activateCode) {
        ProfilerInterface.disableProfilerHooks(); // Just in case
        initInternals();

        // Start the communication thread and wait for it to establish connection with client
        profilerServer = new ProfilerServer(_portNo, activateCode == ATTACH_DYNAMIC, _timeOut);
        profilerServer.start();

        while (!(connectionOpen || connectionFailed)) {
            delay(100);
        }

        if (connectionFailed) {
            if (activateCode == ATTACH_DIRECT) {
                System.exit(-1);
            } else {
                return; // in dynamic attach we just continue with execution
            }
        }

        ProfilerInterface.setProfilerServer(profilerServer);

        initSupportingFunctionality(true);

        if (_activateCode == ATTACH_DIRECT) {
            // "Attach on startup", where we normally wait until the initiate instrumentation request arrives and instrumentation starts.
            // However, the user can also choose to resume the target app without any instrumentation
            while ((ProfilerInterface.getCurrentInstrType() == INSTR_NONE) && !status.targetAppRunning) {
                delay(200);
            }

            delay(100); // Wait a bit more to make sure the classLoadHook is really set
        }

        status.targetAppRunning = true;
    }

    private static void forcedShutdown() {
        cleanupOnShutdown();
        preemptExit = false;
        System.exit(-1);
    }

    private static void initInternals() {
        shutdownWaitThread = new ShutdownWaitThread();
        Runtime.getRuntime().addShutdownHook(shutdownWaitThread);
        profilerInterfaceInitialized = false;
        connectionOpen = false;
        connectionFailed = false;
        detachCommandReceived = false;
        profilerServer = null;
        status = null;
        startTargetApp = false;
        startupException = null;

        // Preload this class, to avoid possible strange problems that may happen in case of wire protocol errors, that in
        // turn may cause loading of this class, that in turn may invoke classLoadHook, etc.
        try {
            Class.forName("java.net.SocketException"); // NOI18N
        } catch (ClassNotFoundException ex) { /* Shouldn't happen */
        }

        // Preload this class, to avoid possible strange problems that happen during Entire App CPU profiling of tomcat,
        // where classLoadHook is invoked during processing GET_DEFINING_CLASSLOADER request
        try {
            Class.forName("java.util.AbstractList$Itr"); // NOI18N
        } catch (ClassNotFoundException ex) { /* Shouldn't happen */
        }

        ThreadInfo.clearProfilerServerThreads();
    }

    /**
     * Called after the connection with the tool is established, i.e. we know that we are connected, in which mode
     * (attached or called directly) and whether it's local or remote connection.
     */
    private static void initSupportingFunctionality(boolean inAttachedMode) {
        status = new ProfilingSessionStatus();
        status.runningInAttachedMode = inAttachedMode;
        status.targetJDKVersionString = Platform.getJDKVersionString();

        Monitors.initialize(); // Initialize before initProfilerInterface to get monitor thread(s) recorded as system thread(s)
                               // Also initialize before initProfilerInterface, same purpose

        profilerServer.initSeparateCmdExecutionThread();
        ThreadInfo.addProfilerServerThread(shutdownWaitThread);
        // Profiler interface initialization includes recording profiler's own threads (all currently running threads minus the
        // current thread, since it will become the target app's main thread).
        ProfilerInterface.initProfilerInterface(status, inAttachedMode ? profilerServer : Thread.currentThread());

        profilerInterfaceInitialized = true;
    }

    private static void pressEnterToShutDown() {
        // Make sure any excessive previous input doesn't cause us to shut down immediately
        try {
            while (System.in.available() > 0) {
                System.in.read();
            }
        } catch (IOException ex) {
            // ignore
        }

        System.out.println(ENTER_TO_SHUTDOWN_MSG);

        try {
            System.in.read();
        } catch (IOException ex) {
            // ignore
        }
    }

    private static void runTargetApp(String mainClassName, String[] mainArgs) {
        Class targetMainClass = null;

        try {
            targetMainClass = ClassLoader.getSystemClassLoader().loadClass(mainClassName);
        } catch (ClassNotFoundException ex) {
            startupException = ex;
            System.err.println(ex);

            return;
        }

        // For the reasons I don't quite understand, if the main class is not public, then somewhere (when we attempt to invoke the
        // main method using reflection?) we get the following: "java.lang.IllegalAccessException: Class org.netbeans.lib.profiler.server.ProfilerServer
        // can not access a member of class Test with modifiers "public static"". Thus we have to run the below preemptive check. Hope this is not
        // a problem for the majority of our users...
        if (!Modifier.isPublic(targetMainClass.getModifiers())) {
            startupException = new IllegalAccessException(MessageFormat.format(MAIN_CLASS_NOT_PUBLIC_MSG, new Object[] { targetMainClass }));
            System.err.println(startupException);

            return;
        }

        Method targetMainMethod = null;
        Class[] params = new Class[] { String[].class };

        try {
            targetMainMethod = targetMainClass.getDeclaredMethod("main", params); // NOI18N
        } catch (NoSuchMethodException ex) {
            startupException = ex;
            System.err.println(ex);

            return;
        }

        // Check for correct method modifiers, to (hopefully) avoid IllegalAccessException and IllegalArgumentException
        int mod = targetMainMethod.getModifiers();

        if (!(Modifier.isPublic(mod) && Modifier.isStatic(mod)) || Modifier.isAbstract(mod) || Modifier.isInterface(mod)) {
            startupException = new IllegalAccessException(MessageFormat.format(INCORRECT_MAIN_MODIFIERS_MSG, new Object[] { targetMainClass }));
            System.err.println(startupException);

            return;
        }

        // We hope after our checks the only exceptions that can be thrown by the target app are those that it generates for
        // natural reasons, and which we should not report as "failed to start the application"
        status.targetAppRunning = true;

        synchronized (targetAppRunningLock) {
            targetAppRunningLock.notify();
        }

        long startTime = Timers.getCurrentTimeInCounts();

        try {
            targetMainMethod.invoke(targetMainClass, new Object[] { mainArgs });
        } catch (IllegalAccessException e1) {
            startupException = e1;
            System.err.println(e1);
        } catch (IllegalArgumentException e2) {
            startupException = e2;
            System.err.println(e2);
        } catch (InvocationTargetException e3) {
            Throwable cause = e3.getCause();

            if (cause != null) {
                cause.printStackTrace(System.err);
            } else { // Can this ever happen?
                internalError("Target application threw a null exception?"); // NOI18N
            }
        } catch (Throwable ex) {
            ProfilerInterface.disableProfilerHooks();
            internalError(MessageFormat.format(UNEXPECTED_EXCEPTION_MSG, new Object[] { ex }), false);
            ex.printStackTrace(System.err);
        } finally {
            int elapsedTime = (int) (((Timers.getCurrentTimeInCounts() - startTime) * 1000) / Timers.getNoOfCountsInSecond());
            System.out.println(MessageFormat.format(ELAPSED_TIME_MSG, new Object[] { "" + elapsedTime })); // NOI18N
        }
    }

    private static void waitForShutdownOK() {
        synchronized (shutdownLock) {
            while (!shutdownOK && !Thread.interrupted()) {
                try {
                    shutdownLock.wait(500);
                } catch (InterruptedException e) {
                }

                Thread.yield();
            }

            if (shutdownOK) {
                return;
            }
        }

        System.err.println("ProfilerServer hasn't shut down cleanly. Terminated."); // NOI18N

        //    while (true) {
        //      if (shutdownOK) {
        //        return;
        //      }
        //      delay(100);
        //    }
    }

    private int getAgentId() {
        if (agentId == -1) {
            String id = System.getProperty("nbprofiler.agentid"); // NOI18N

            if (id != null) {
                try {
                    agentId = Integer.parseInt(id);
                } catch (NumberFormatException e) {
                    System.err.println(MessageFormat.format(INCORRECT_AGENT_ID_MSG, new Object[] { id }));
                                                                                                           // ignore, the agentId will be generated randomly
                }
            }

            if (agentId == -1) {
                agentId = r.nextInt(Integer.MAX_VALUE);
            }
        }

        return agentId;
    }

    private static void setLastResponse(Response r) {
        synchronized (responseLock) {
            lastResponse = r;

            try {
                responseLock.notify();
            } catch (IllegalMonitorStateException ex) {
                internalError("IllegalMonitorState in ProfilerServer.setLastResponse()"); // NOI18N
            }
        }
    }

    private static String getLocalizedJFluidServerJar(String jfluidServerDir) {
        String localizedJFluidServerJar = null;

        // normalize provided directory to use forward slashes with slash at the end of path
        String baseDir = jfluidServerDir.replace('\\', '/'); // NOI18N

        if (!baseDir.endsWith("/")) { // NOI18N
            baseDir = baseDir + "/"; // NOI18N
        }

        // check if directory exists
        File baseDirF = new File(baseDir);

        if (!baseDirF.exists() || !baseDirF.isDirectory()) {
            return null;
        }

        // check if locale directory exists
        String localeDir = baseDir + "locale/"; // NOI18N
        File localeDirF = new File(localeDir);

        if (localeDirF.exists() && localeDirF.isDirectory()) {
            // locale directory found, try to find jar inside
            localizedJFluidServerJar = getLocalizedJFluidServerJarInDir(localeDir);

            if (localizedJFluidServerJar != null) {
                return localizedJFluidServerJar;
            }
        }

        // locale directory doesn't exist or jar not found in it, try to find jar directly in jfluid dir
        localizedJFluidServerJar = getLocalizedJFluidServerJarInDir(baseDir);

        return localizedJFluidServerJar;
    }

    private static String getLocalizedJFluidServerJarInDir(String jfluidServerLocaleDir) {
        LocaleIterator localeIterator = new LocaleIterator(Locale.getDefault());
        String jarFile;
        File jarFileF;

        while (localeIterator.hasNext()) {
            jarFile = jfluidServerLocaleDir + "jfluid-server" + localeIterator.next() + ".jar"; // NOI18N
            jarFileF = new File(jarFile);

            if (jarFileF.exists() && jarFileF.isFile()) {
                return jarFile;
            }
        }

        return null;
    }

    // Does not support branding!
    private static ResourceBundle getProfilerServerResourceBundle(String jfluidPath) {
        ResourceBundle bundle = null;

        if (jfluidPath == null) {
            throw new RuntimeException("ProfilerServer: Unable to initialize ResourceBundle for ProfilerServer, " // NOI18N
                                       + "cannot find path to Profiler libraries" // NOI18N
                                       );
        }

        String jfluidServerJar = getLocalizedJFluidServerJar(jfluidPath);

        if (jfluidServerJar == null) {
            throw new RuntimeException("ProfilerServer: Unable to initialize ResourceBundle for ProfilerServer, " // NOI18N
                                       + "cannot find localized jfluid-server.jar" // NOI18N
                                       );
        }

        try {
            if (!jfluidServerJar.startsWith("/")) { // NOI18N
                jfluidServerJar = "/" + jfluidServerJar; // NOI18N
            }

            String bundleJarURLPath = "jar:file:" + jfluidServerJar + "!/"; // NOI18N
            URLClassLoader loader = new URLClassLoader(new URL[] { new URL(bundleJarURLPath) });
            bundle = ResourceBundle.getBundle("org.netbeans.lib.profiler.server.Bundle", Locale.getDefault(), loader); // NOI18N
        } catch (Exception e2) {
            throw new RuntimeException("ProfilerServer: Unable to initialize ResourceBundle for ProfilerServer\n" + e2.getMessage()); // NOI18N
        }

        if (bundle == null) {
            throw new RuntimeException("ProfilerServer: Unable to initialize ResourceBundle for ProfilerServer" // NOI18N
            );
        }

        return bundle;
    }

    private synchronized void closeConnection() {
        connectionOpen = false;
        if (status != null) {
            status.targetAppRunning = false;
        }
        removeInfoFile();

        try {
            socketOut.close();
            socketIn.close();
            clientSocket.close();
            serverSocket.close();
        } catch (IOException ex) {
        }

        if (status == null) {
            System.out.println(CONNECTION_CLOSED_EX_MSG);
        } else if (status.runningInAttachedMode) {
            System.out.println(CONNECTION_CLOSED_MSG);
        }

        preemptExit = false;
    }

    private boolean connectToClient() {
        try {
            if (serverTimeout == 0) {
                System.out.println(MessageFormat.format(WAITING_ON_PORT_MSG,
                                                        new Object[] { "" + serverPort, // NOI18N
                                                                       "" + CURRENT_AGENT_VERSION // NOI18N
                                                        }));
            } else {
                System.out.println(MessageFormat.format(WAITING_ON_PORT_TIMEOUT_MSG,
                                                        new Object[] { "" + serverPort, // NOI18N
                                                                       "" + serverTimeout, // NOI18N
                                                                       "" + CURRENT_AGENT_VERSION // NOI18N
                                                        }));
            }

            serverSocket = new ServerSocket(serverPort);
            serverSocket.setSoTimeout(serverTimeout * 1000); // serverTimeout is in seconds
            createInfoFile();
            clientSocket = serverSocket.accept();
            clientSocket.setTcpNoDelay(true); // Necessary at least on Solaris to avoid delays in e.g. readInt() etc.
            socketIn = new ObjectInputStream(clientSocket.getInputStream());
            socketOut = new ObjectOutputStream(clientSocket.getOutputStream());
            wireIO = new WireIO(socketOut, socketIn);
            connectionOpen = true;
            System.out.println(CONNECTION_MSG);
            return true;
        } catch (SocketTimeoutException ex) {
            System.err.println(CONNECTION_TIMEOUT_MSG);
            connectionFailed = true;
        } catch (IOException ex) {
            System.err.println(MessageFormat.format(CONNECTION_EXCEPTION_MSG, new Object[] { ex }));
            if (ex instanceof BindException) System.err.println(CONNECTION_EXCEPTION_BIND_MSG);
            connectionFailed = true;
        } finally {
            //removeInfoFile ();
        }

        return false;
    }

    private void createInfoFile() {
        BufferedOutputStream bos = null;

        try {
            File f = getInfoFile(serverPort);
            f.createNewFile();
            f.deleteOnExit();

            Properties props = new Properties();
            props.setProperty("dynamic", Boolean.toString(dynamic)); // NOI18N
            props.setProperty("working.dir", System.getProperty("user.dir")); // NOI18N
            props.setProperty("agent.id", Integer.toString(getAgentId())); // NOI18N
            props.setProperty("java.version", System.getProperty("java.version")); // NOI18N

            FileOutputStream fos = new FileOutputStream(f);
            bos = new BufferedOutputStream(fos);

            props.store(bos, ""); // NOI18N

            bos.close();
        } catch (IOException e) {
            System.err.println(MessageFormat.format(AGENT_ERROR_MSG, new Object[] { e.getMessage() }));
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Some of the commands need to be executed in a separate thread, because they result in the server sending something
     * to the client and awaiting its response. The response, in turn, can only be picked up by the single JFluid communication
     * thread. So we execute these commands in a separate thread to allow the main communication thread to return immediately,
     * and be ready to process client's response.
     */
    private void executeInSeparateThread(int opCode) {
        synchronized (execInSeparateThreadLock) {
            execInSeparateThreadOpCode = opCode;

            try {
                execInSeparateThreadLock.notify();
            } catch (IllegalMonitorStateException ex) {
                System.err.println(THREAD_EXCEPTION_MSG);
            }
        }
    }

    //---------------------------------------------------------------------------------------
    // Command/response handling
    //---------------------------------------------------------------------------------------
    private void handleClientCommand(Command cmd) {
        //System.out.println(">>> Got command " + cmd);
        if (cmd.getType() == Command.START_TARGET_APP) {
            if (status.runningInAttachedMode) {
                // This is a special case - the user has chosen "Attach on startup" and then "resume application without instrumentation"
                status.targetAppRunning = true;
                sendSimpleResponseToClient(true, null);

                return;
            }

            // Start target app is handled by a separate thread, since we want to return to the client a synchronous response telling
            // whether or not the target app was started successfully. To get an answer to this question, we have to wait until the main
            // class is loaded, its main method is found, etc. Only after that the targetAppRunningLock.notify() is called. Until then
            // this thread remains blocked. However, if instrumentation root method == main method, class load hook is invoked immediately
            // when the main class is loaded. Class load hook, in turn, sends a RootClassLoaded command to the server and waits for the
            // response. But responses are read by the same thread that calls handleClientCommand(). So if we do the below operations in
            // the same thread, we deadlock - so, a separate thread is needed to allow the main listener thread to handle incoming
            // commands/responses immediately.
            class MyThread extends Thread {
                MyThread() {
                    ThreadInfo.addProfilerServerThread(this);
                    this.setName(PROFILER_SPECIAL_EXEC_THREAD_NAME + " 4"); // NOI18N
                }

                public void run() {
                    synchronized (targetAppRunningLock) {
                        startTargetApp = true;

                        try {
                            targetAppRunningLock.wait();
                        } catch (InterruptedException ex) {
                            internalError("START_TARGET_APP"); // NOI18N
                        }
                    }

                    if (startupException != null) {
                        sendSimpleResponseToClient(false, startupException.toString());
                    } else {
                        sendSimpleResponseToClient(true, null);
                    }

                    ThreadInfo.removeProfilerServerThread(this);
                }
            }
            new MyThread().start();

            return;
        }

        switch (cmd.getType()) {
            case Command.GET_MONITORED_NUMBERS:
                sendComplexResponseToClient(Monitors.getMonitoredNumbers());

                break;
            case Command.INITIATE_PROFILING:

                // Bugfix 69645: Take snapshot is not enabled after modifying profiling from CPU to memory
                // http://profiler.netbeans.org/issues/show_bug.cgi?id=69645
                synchronized (resultsNotifiedLock) {
                    resultsNotified = false;
                }

                try {
                    ProfilerInterface.initiateProfiling((InitiateProfilingCommand) cmd, status.targetAppRunning);
                    sendSimpleResponseToClient(true, null);
                } catch (Exception ex) {
                    sendSimpleResponseToClient(false, ex.getMessage());
                }

                break;
            case Command.INSTRUMENT_METHOD_GROUP:
                class InstrumentMethodGroupThread extends Thread {
                    final InstrumentMethodGroupCommand methodGroupCmd;
                    String exceptionString;

                    InstrumentMethodGroupThread(InstrumentMethodGroupCommand cmd) {
                        ThreadInfo.addProfilerServerThread(this);
                        setName(PROFILER_SPECIAL_EXEC_THREAD_NAME + " 8"); // NOI18N
                        methodGroupCmd = cmd;
                    }

                    public void run() {
                        try {
                            ProfilerInterface.instrumentMethods(methodGroupCmd);
                        } catch (Exception ex) {
                            exceptionString = ex.getLocalizedMessage();
                        }

                        ThreadInfo.removeProfilerServerThread(this);
                    }
                }

                InstrumentMethodGroupThread instrumentMethodGroupThread = new InstrumentMethodGroupThread((InstrumentMethodGroupCommand) cmd);
                instrumentMethodGroupThread.start();

                while(instrumentMethodGroupThread.isAlive()) {
                    delay(2000);
                    sendSimpleCmdToClient(Command.STILL_ALIVE);
                }

                if (instrumentMethodGroupThread.exceptionString != null) {
                    sendSimpleResponseToClient(false, instrumentMethodGroupThread.exceptionString);
                } else {
                    sendSimpleResponseToClient(true, null);
                }

                break;
            case Command.CHECK_CONNECTION:
                sendSimpleResponseToClient(true, null);

                break;
            case Command.SET_CHANGEABLE_INSTR_PARAMS:
                boolean threadSampling;
                boolean waitTracking;
                boolean sleepTracking;
                boolean lockContentionMonitoring;
                SetChangeableInstrParamsCommand scipCmd = (SetChangeableInstrParamsCommand) cmd;
                lockContentionMonitoring = scipCmd.isLockContentionMonitoringEnabled();
                ProfilerRuntime.setLockContentionMonitoringEnabled(lockContentionMonitoring);
                ProfilerRuntimeCPU.setNProfiledThreadsLimit(scipCmd.getNProfiledThreadsLimit());
                ProfilerRuntimeCPU.setStackDepthLimit(scipCmd.getStackDepthLimit());
                ProfilerRuntimeCPUSampledInstr.setSamplingInterval(scipCmd.getSamplingInterval());
                ProfilerRuntimeSampler.setSamplngFrequency(scipCmd.getThreadsSamplingFrequency());
                ProfilerRuntimeMemory.setSamplingInterval((short) scipCmd.getObjAllocStackSamplingInterval());
                ProfilerRuntimeMemory.setStackSamplingDepth(scipCmd.getObjAllocStackSamplingDepth());
                ProfilerRuntimeObjLiveness.setRunGCOnGetResults(scipCmd.getRunGCOnGetResultsInMemoryProfiling());
                threadSampling = scipCmd.isThreadsSamplingEnabled();
                waitTracking = scipCmd.isWaitTrackingEnabled();
                sleepTracking = scipCmd.isSleepTrackingEnabled();
                Monitors.setThreadsSamplingEnabled(threadSampling);
                ProfilerRuntimeCPU.setWaitAndSleepTracking(waitTracking,sleepTracking);
                Classes.setWaitTrackingEnabled(threadSampling || waitTracking || lockContentionMonitoring);
                Classes.setParkTrackingEnabled(threadSampling || waitTracking);
                Classes.setSleepTrackingEnabled(threadSampling || sleepTracking);
                sendSimpleResponseToClient(true, null);

                break;
            case Command.SET_UNCHANGEABLE_INSTR_PARAMS:

                SetUnchangeableInstrParamsCommand sucipCmd = (SetUnchangeableInstrParamsCommand) cmd;
                ProfilerRuntimeCPU.setTimerTypes(sucipCmd.getAbsoluteTimerOn(), sucipCmd.getThreadCPUTimerOn());
                status.instrScheme = sucipCmd.getInstrScheme();
                ProfilerRuntimeCPUCodeRegion.setCPUResBufSize(sucipCmd.getCodeRegionCPUResBufSize());
                ProfilerRuntimeCPU.enableFirstTimeMethodInvoke(status.instrScheme != INSTRSCHEME_TOTAL);
                setRemoteProfiling(sucipCmd.getRemoteProfiling());
                sendSimpleResponseToClient(true, null);

                break;
            case Command.CPU_RESULTS_EXIST:
                sendSimpleResponseToClient(ProfilerInterface.cpuResultsExist(), null);

                break;
            case Command.DUMP_EXISTING_RESULTS:
            case Command.DUMP_EXISTING_RESULTS_LIVE:
                // We have to execute the dump in a separate thread to make this call (handleClientCommand()) return immediately.
                // Otherwise, it would not allow the server to receive a response from the client, that the client sends when it
                // processes the dumped results. Generally, all commands that may call ProfilerRuntime.dumpEventBuffer() should be
                // executed in a separate thread.
                executeInSeparateThread(cmd.getType());

                break;
            case Command.GET_CODE_REGION_CPU_RESULTS:
                sendComplexResponseToClient(ProfilerInterface.getCodeRegionCPUResults());

                break;
            case Command.GET_OBJECT_ALLOCATION_RESULTS:
                sendComplexResponseToClient(ProfilerInterface.getObjectAllocationResults());

                break;
            case Command.GET_METHOD_NAMES_FOR_JMETHOD_IDS:

                GetMethodNamesForJMethodIdsCommand gmnCmd = (GetMethodNamesForJMethodIdsCommand) cmd;
                sendComplexResponseToClient(ProfilerInterface.getMethodNamesForJMethodIds(gmnCmd.getMethodIds()));

                break;
            case Command.RESET_PROFILER_COLLECTORS:
                // Since the resetProfilerCollectors() eventually invokes the dump results method, which in turn sends a command to the client
                // and awaits response, we have to execute it in a separate thread. See comments in DUMP_EXISTING_RESULTS above.
                executeInSeparateThread(cmd.getType());

                break;
            case Command.DEACTIVATE_INJECTED_CODE:
                ProfilerInterface.deactivateInjectedCode();
                sendSimpleResponseToClient(true, null);

                break;
            case Command.GET_THREAD_LIVENESS_STATUS:
                sendComplexResponseToClient(ProfilerInterface.getCurrentThreadLivenessStatus());

                break;
            case Command.SUSPEND_TARGET_APP:
                ProfilerInterface.suspendTargetApp();
                sendSimpleResponseToClient(true, null);

                break;
            case Command.RESUME_TARGET_APP:
                ProfilerInterface.resumeTargetApp();
                sendSimpleResponseToClient(true, null);

                break;
            case Command.TERMINATE_TARGET_JVM:

                if (ProfilerInterface.getCurrentInstrType() != INSTR_NONE) {
                    ProfilerInterface.deactivateInjectedCode();
                }

                sendSimpleResponseToClient(true, null);
                closeConnection();
                preemptExit = false;
                doExit();

                break;
            case Command.SHUTDOWN_OK:
                setShutdownOK();

                break;
            case Command.INSTRUMENT_REFLECTION:
                ProfilerInterface.setInstrumentReflection(true);
                sendSimpleResponseToClient(true, null);

                break;
            case Command.DEINSTRUMENT_REFLECTION:
                ProfilerInterface.setInstrumentReflection(false);
                sendSimpleResponseToClient(true, null);

                break;
            case Command.RUN_GC:
                GC.runGC();
                sendSimpleResponseToClient(true, null);

                break;
            case Command.GET_DEFINING_CLASS_LOADER: {
                GetDefiningClassLoaderCommand gdclCmd = (GetDefiningClassLoaderCommand) cmd;
                int loaderId = ClassLoaderManager.getDefiningLoaderForClass(gdclCmd.getClassName(), gdclCmd.getClassLoaderId());
                DefiningLoaderResponse resp = new DefiningLoaderResponse(loaderId);
                sendComplexResponseToClient(resp);

                break;
            }
            case Command.GET_VM_PROPERTIES: {
                status.jvmArguments = Threads.getJVMArguments();
                status.javaCommand = Threads.getJavaCommand();

                VMPropertiesResponse resp = new VMPropertiesResponse(Platform.getJavaVersionString(), 
                                                                     System.getProperty("java.class.path"), // NOI18N
                                                                     System.getProperty("java.ext.dirs", ""), // NOI18N
                                                                     System.getProperty("sun.boot.class.path", ""), // NOI18N
                                                                     System.getProperty("user.dir"), // NOI18N
                                                                     status.jvmArguments, status.javaCommand,
                                                                     System.getProperty("os.name"), // NOI18N
                                                                     InstrumentConstructorTest.test(),
                                                                     Runtime.getRuntime().maxMemory(),
                                                                     System.currentTimeMillis(), Timers.getCurrentTimeInCounts(),
                                                                     getAgentId()
                );
                sendComplexResponseToClient(resp);

                break;
            }
            case Command.GET_STORED_CALIBRATION_DATA: { // called in the beginning of remote CPU profiling

                int ret = CalibrationDataFileIO.readSavedCalibrationData(status);

                if (ret == 0) {
                    CalibrationDataResponse resp = new CalibrationDataResponse(status.methodEntryExitCallTime,
                                                                               status.methodEntryExitInnerTime,
                                                                               status.methodEntryExitOuterTime,
                                                                               status.timerCountsInSecond);
                    profilerServer.sendComplexResponseToClient(resp);
                } else {
                    sendSimpleResponseToClient(false, CalibrationDataFileIO.getErrorMessage());
                }

                break;
            }
            case Command.RUN_CALIBRATION_AND_GET_DATA: {
                ProfilerCalibrator.init(status);
                ProfilerCalibrator.measureBCIOverhead(false);

                CalibrationDataResponse resp = new CalibrationDataResponse(status.methodEntryExitCallTime,
                                                                           status.methodEntryExitInnerTime,
                                                                           status.methodEntryExitOuterTime,
                                                                           status.timerCountsInSecond);
                profilerServer.sendComplexResponseToClient(resp);

                break;
            }
            case Command.GET_INTERNAL_STATS:
                ProfilerCalibrator.init(status);
                sendComplexResponseToClient(ProfilerCalibrator.getInternalStats());

                break;
            case Command.PREPARE_DETACH:
                ProfilerInterface.setDetachStarted(true); //inform other threads they should stop ongoing instrumentation
                boolean success = ProfilerInterface.serialClientOperationsLock.beginTrans(true, true);
                sendSimpleResponseToClient(success, null);                
                break;
            case Command.DETACH:
                if(ProfilerInterface.isDetachStarted()) {
                    ProfilerInterface.serialClientOperationsLock.endTrans();
                }
                ProfilerInterface.setDetachStarted(false);

                // Just in case, normally should be deactivated and cleaned up by client
                ProfilerInterface.deactivateInjectedCode();
                ProfilerInterface.disableProfilerHooks();
                ProfilerInterface.clearProfilerDataStructures();

                stopSeparateCmdExecutionThread();
                Monitors.shutdown();
                ThreadInfo.clearProfilerServerThreads();
                detachCommandReceived = true;
                sendSimpleResponseToClient(true, null);

                break;
            case Command.TAKE_HEAP_DUMP:

                TakeHeapDumpCommand dumpCmd = (TakeHeapDumpCommand) cmd;
                String error = HeapDump.takeHeapDump(dumpCmd.getOutputFile());

                sendSimpleResponseToClient(error == null, error);

                break;
            case Command.GET_HEAP_HISTOGRAM:
                Response resp = ProfilerInterface.computeHistogram();
                sendComplexResponseToClient(resp);
                
                break;
            case Command.TAKE_THREAD_DUMP:
                Response tdResp = new ThreadDumpResponse(ThreadDump.isJDK15(), new Date(), ThreadDump.takeThreadDump());
                sendComplexResponseToClient(tdResp);
                
                break;
            case Command.GET_CLASS_FILE_BYTES:
                //System.out.println(cmd);
                GetClassFileBytesCommand getCmd = (GetClassFileBytesCommand) cmd;
                byte[][] bytes = ProfilerInterface.getClassFileBytes(getCmd.getClasses(), getCmd.getClassLoaderIds());
                sendComplexResponseToClient(new GetClassFileBytesResponse(bytes));
                
                break;
        }
    }

    private void doExit() {

        // try Lookup.getDefault().lookup(ClassLoader.class)
        try {
            Class lookupClz = Thread.currentThread().getContextClassLoader().loadClass("org.openide.util.Lookup"); // NOI18N
            Method instMethod = lookupClz.getMethod("getDefault", new Class[0]); // NOI18N
            Method lookupMethod = lookupClz.getMethod("lookup", new Class[]{Class.class}); // NOI18N

            Object instance = instMethod.invoke(lookupClz, new Object[0]);
            if (instance != null) {
                ClassLoader clInstance = (ClassLoader)lookupMethod.invoke(instance, new Class[]{ClassLoader.class});
                if (clInstance != null) {
                    // try to call Ide.quit in JDeveloper
                    try {
                        Class oracleIde = clInstance.loadClass("oracle.ide.Ide"); // NOI18N
                        Method ideQuitMethod = oracleIde.getMethod("quit", new Class[0]); // NOI18N
                        ideQuitMethod.invoke(oracleIde, new Object[0]);
                        return;
                    } catch (Exception ex) {
                        // ignore
                    }
                    // try to call LifecycleManager in NetBeans
                    Class lcmInstanceClz = clInstance.loadClass("org.openide.LifecycleManager"); // NOI18N
                    Method lcmInstMethod = lcmInstanceClz.getMethod("getDefault", new Class[0]); // NOI18N
                    Method lcmExitMethod = lcmInstanceClz.getMethod("exit", new Class[0]); // NOI18N
                    Object lcmInstance = lcmInstMethod.invoke(lcmInstanceClz, new Object[0]);
                    lcmExitMethod.invoke(lcmInstance, new Object[0]);
                    return;
                }
            }
        } catch (Exception ex) {
            // ignore
        }
        
        // fall through a general system exit
        System.exit(-1);
    }

    private void handleIOExceptionOnSend(IOException ex) {
        System.err.println(MessageFormat.format(RESPONSE_EXCEPTION_MSG, new Object[] { ex }));
        ex.printStackTrace(System.err);
        closeConnection();
    }

    private void initSeparateCmdExecutionThread() {
        separateCmdExecutionThread = new SeparateCmdExecutionThread();
        separateCmdExecutionThread.start();
    }

    private static void internalError(String message) {
        internalError(message, true);
    }

    private static void internalError(String message, boolean exit) {
        System.err.println("Profiler Engine Error: " + message); // NOI18N

        if (exit) {
            preemptExit = false;
            System.exit(-1);
        }
    }

    private void listenToClient() {
        while (connectionOpen && !detachCommandReceived) {
            try {
                Object o = wireIO.receiveCommandOrResponse();

                if (o == null) {
                    System.err.println(CONNECTION_INTERRUPTED_MSG);

                    break; // end of connection
                }

                //System.out.println(">>> Profiler Engine: received command or response " + o);
                if (o instanceof Command) {
                    handleClientCommand((Command) o);
                } else {
                    setLastResponse((Response) o);
                }
            } catch (IOException ex) {
                if (connectionOpen && !detachCommandReceived) { // It is not an asynchronous connection shutdown
                    System.err.println(MessageFormat.format(COMMAND_EXCEPTION_MSG, new Object[] { ex }));
                }

                break;
            }
        }

        closeConnection();
    }

    private void removeInfoFile() {
        try {
            getInfoFile(serverPort).delete();
        } catch (IOException e) {
            System.err.println(MessageFormat.format(AGENT_ERROR_MSG, new Object[] { e.getMessage() }));
        }
    }

    private void stopSeparateCmdExecutionThread() {
        separateCmdExecutionThread.terminate();

        synchronized (execInSeparateThreadLock) {
            try {
                execInSeparateThreadLock.notify();
            } catch (IllegalMonitorStateException ex) {
            }
        }
    }
    
    private void resetResultsNotifiedFlag() {
        synchronized (resultsNotifiedLock) {
            resultsNotified = false;
        }
    }

    boolean isDynamic() {
        return dynamic;
    }
}
