com.izforge.izpack.util.SelfModifier.java Source code

Java tutorial

Introduction

Here is the source code for com.izforge.izpack.util.SelfModifier.java

Source

/*
 * IzPack - Copyright 2001-2013 Julien Ponge, All Rights Reserved.
 *
 * http://izpack.org/
 * http://izpack.codehaus.org/
 *
 * Copyright 2004 Chadwick McHenry
 *
 * Licensed 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 com.izforge.izpack.util;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

import java.io.*;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

/**
 * Allows an application to modify the jar file from which it came, including outright deletion. The
 * jar file of an app is usually locked when java is run so this is normally not possible.
 * <p/>
 * <p/>
 * Create a SelfModifier with a target method, then invoke the SelfModifier with arguments to be
 * passed to the target method. The jar file containing the target method's class (obtained by
 * reflection) will be extracted to a temporary directory, and a new java process will be spawned to
 * invoke the target method. The original jar file may now be modified.
 * <p/>
 * <p/>
 * If the constructor or invoke() methods fail, it is generally because secondary java processes
 * could not be started.
 * <p/>
 * <b>Requirements</b>
 * <ul>
 * <li>The target method, and all it's required classes must be in a jar file.
 * <li>The Self Modifier, and its inner classes must also be in the jar file.
 * </ul>
 * <p/>
 * There are three system processes (or "phases") involved, the first invoked by the user, the
 * second and third by the SelfModifier.
 * <p/>
 * <p/>
 * <b>Phase 1:</b>
 * <ol>
 * <li>Program is launched, SelfModifier is created, invoke(String[]) is called
 * <li>A temporary directory (or "sandbox") is created in the default temp directory, and the jar
 * file contents ar extracted into it
 * <li>Phase 2 is spawned using the sandbox as it's classpath, SelfModifier as the main class, the
 * arguments to "invoke(String[])" as the main arguments, and the <a
 * href="#selfmodsysprops">SelfModifier system properties</a> set.
 * <li>Immediately exit so the system unlocks the jar file
 * </ol>
 * <p/>
 * <b>Phase 2:</b>
 * <ol>
 * <li>Initializes from system properties.
 * <li>Spawn phase 3 exactly as phase 2 except the self.modifier.phase system properties set to 3.
 * <li>Wait for phase 3 to die
 * <li>Delete the temporary sandbox
 * </ol>
 * <p/>
 * <b>Phase 3:</b>
 * <ol>
 * <li>Initializes from system properties.
 * <li>Redirect std err stream to the log
 * <li>Invoke the target method with arguments we were given
 * <li>The target method is expected to call exit(), or to not start any looping threads (e.g. AWT
 * thread). In other words, the target is the new "main" method.
 * </ol>
 * <p/>
 * <a name="selfmodsysprops"><b>SelfModifier system properties</b></a> used to pass information
 * between processes. <table border="1">
 * <tr>
 * <th>Constant
 * <th>System property
 * <th>description</tr>
 * <tr>
 * <td><a href="#BASE_KEY">BASE_KEY</a>
 * <td>self.mod.jar
 * <td>base path to log file and sandbox dir</tr>
 * <tr>
 * <td><a href="#JAR_KEY">JAR_KEY</a>
 * <td>self.mod.class
 * <td>path to original jar file</tr>
 * <tr>
 * <td><a href="#CLASS_KEY">CLASS_KEY</a>
 * <td>self.mod.method
 * <td>class of target method</tr>
 * <tr>
 * <td><a href="#METHOD_KEY">METHOD_KEY</a>
 * <td>self.mod.phase
 * <td>name of method to be invoked in sandbox</tr>
 * <tr>
 * <td><a href="#PHASE_KEY">PHASE_KEY</a>
 * <td>self.mod.base
 * <td>phase of operation to run</tr>
 * </table>
 *
 * @author Chadwick McHenry
 * @version 1.0
 */
public class SelfModifier {

    /**
     * System property name of base for log and sandbox of secondary processes.
     */
    private static final String BASE_KEY = "self.mod.base";

    /**
     * System property name of original jar file containing application.
     */
    private static final String JAR_KEY = "self.mod.jar";

    /**
     * System property name of class declaring target method.
     */
    private static final String CLASS_KEY = "self.mod.class";

    /**
     * System property name of target method to invoke in secondary process.
     */
    private static final String METHOD_KEY = "self.mod.method";

    /**
     * System property name of phase (1, 2, or 3) indicator.
     */
    private static final String PHASE_KEY = "self.mod.phase";

    /**
     * Target method to be invoked in sandbox.
     */
    private Method method = null;

    /**
     * Log for phase 2 and 3, because we can't capture the stdio from them.
     */
    private File logFile = null;

    /**
     * Directory which we extract too, invoke from, and finally delete.
     */
    private File sandbox = null;

    /**
     * Original jar file program was launched from.
     */
    private File jarFile = null;

    /**
     * Current phase of execution: 1, 2, or 3.
     */
    private int phase = 0;

    /**
     * For logging time.
     */
    private final SimpleDateFormat isoPoint = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");

    private final Date date = new Date();

    /**
     * Debug port for phase 2, or <tt>-1</tt> if not set or invalid
     */
    private final int debugPort2 = Integer.getInteger(DEBUG_PORT2_KEY, -1);

    /**
     * Debug port for phase 3, or <tt>-1</tt> if not set or invalid
     */
    private final int debugPort3 = Integer.getInteger(DEBUG_PORT3_KEY, -1);

    /**
     * System property name of the debug port for phase 2.
     */
    private static final String DEBUG_PORT2_KEY = "self.mod.debugPort2";

    /**
     * System property name of the debug port for phase 3.
     */
    private static final String DEBUG_PORT3_KEY = "self.mod.debugPort3";

    /**
     * Base prefix name for sandbox and log, used only in phase 1.
     */
    private static final String prefix = "izpack";

    public static void test(String[] args) {
        // open a File for random access in the sandbox, which will cause
        // deletion
        // of the file and its parent directories to fail until it is closed (by
        // virtue of this java process halting)
        try {
            File sandbox = new File(System.getProperty(BASE_KEY) + ".d");
            File randFile = new File(sandbox, "RandomAccess.tmp");
            RandomAccessFile rand = new RandomAccessFile(randFile, "rw");
            rand.writeChars("Just a test: The JVM has to close 'cuz I won't!\n");

            System.err.print("Deleting sandbox: ");
            FileUtils.deleteDirectory(sandbox);
            System.err.println(sandbox.exists() ? "FAILED" : "SUCCEEDED");
        } catch (Exception x) {
            System.err.println(x.getMessage());
            x.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // phase 1 already set up the sandbox and spawned phase 2.
        // phase 2 creates the log, spawns phase 3 and waits
        // phase 3 invokes method and returns. method must kill all it's threads

        // all it's attributes are retrieved from system properties
        SelfModifier selfModifier = new SelfModifier();

        // phase 2: invoke a process for phase 3, wait, and clean up
        if (selfModifier.phase == 2) {
            selfModifier.invoke2(args);
        }

        // phase 3: invoke method and die
        else if (selfModifier.phase == 3) {
            selfModifier.invoke3(args);
        }
    }

    /**
     * Internal constructor where target class and method are obtained from system properties.
     *
     * @throws SecurityException if access to the target method is denied
     */
    private SelfModifier() {
        phase = Integer.parseInt(System.getProperty(PHASE_KEY));

        String cName = System.getProperty(CLASS_KEY);
        String tName = System.getProperty(METHOD_KEY);

        jarFile = new File(System.getProperty(JAR_KEY));
        logFile = new File(System.getProperty(BASE_KEY) + ".log");
        sandbox = new File(System.getProperty(BASE_KEY) + ".d");

        // retrieve reference to target method
        try {
            Class<?> clazz = Class.forName(cName);
            Method method = clazz.getMethod(tName, String[].class);

            initMethod(method);
        } catch (ClassNotFoundException x1) {
            log("No class found for " + cName);
        } catch (NoSuchMethodException x2) {
            log("No method " + tName + " found in " + cName);
        }
    }

    /**
     * Creates a SelfModifier which will invoke the target method in a separate process from which
     * it may modify it's own jar file.
     * <p/>
     * The target method must be public, static, and take a single array of strings as its only
     * parameter. The class which declares the method must also be public. Reflection is used to
     * ensure this.
     *
     * @param method a public, static method that accepts a String array as it's only parameter. Any
     *               return value is ignored.
     * @throws NullPointerException     if <code>method</code> is null
     * @throws IllegalArgumentException if <code>method</code> is not public, static, and take a
     *                                  String array as it's only argument, or of it's declaring class is not public.
     * @throws IllegalStateException    if process was not invoked from a jar file,
     *                                  or an IOException occurred while accessing it
     * @throws IOException              if java is unable to be executed as a separate process
     * @throws SecurityException        if access to the method, or creation of a child process is denied
     */
    public SelfModifier(Method method) throws IOException {
        phase = 1;
        ProcessHelper.tryExecJava();
        initMethod(method);
    }

    /**
     * Check the method for the required properties (public, static, params:(String[])).
     *
     * @param method the method
     * @throws NullPointerException     if <code>method</code> is null
     * @throws IllegalArgumentException if <code>method</code> is not public, static, and take a
     *                                  String array as it's only argument, or of it's declaring class is not public.
     * @throws SecurityException        if access to the method is denied
     */
    private void initMethod(Method method) {
        int mod = method.getModifiers();
        if ((mod & Modifier.PUBLIC) == 0 || (mod & Modifier.STATIC) == 0) {
            throw new IllegalArgumentException("Method not public and static");
        }

        Class[] params = method.getParameterTypes();
        if (params.length != 1 || !params[0].isArray()
                || !"java.lang.String".equals(params[0].getComponentType().getName())) {
            throw new IllegalArgumentException("Method must accept String array");
        }

        Class clazz = method.getDeclaringClass();
        mod = clazz.getModifiers();
        if ((mod & Modifier.PUBLIC) == 0 || (mod & Modifier.INTERFACE) != 0) {
            throw new IllegalArgumentException("Method must be in a public class");
        }

        this.method = method;
    }

    /**
     * Invoke the target method in a separate process from which it may modify it's own jar file.
     * This method does not normally return. After spawning the secondary process, the current
     * process must die before the jar file is unlocked, therefore calling this method is akin to
     * calling {@link System#exit(int)}.
     * <p/>
     * <p/>
     * The contents of the current jar file are extracted copied to a 'sandbox' directory from which
     * the method is invoked. The path to the original jar file is placed in the system property
     * {@link #JAR_KEY}.
     * <p/>
     *
     * @param args arguments to pass to the target method. May be empty or null to indicate no
     *             arguments.
     * @throws IOException           for lots of things
     * @throws IllegalStateException if method's class was not loaded from a jar
     */
    public void invoke(String[] args) throws IOException {
        // Initialize sandbox and log file to be unique, but similarly named
        while (true) {
            logFile = File.createTempFile(prefix, ".log");
            System.out.println("The uninstaller has put a log file: " + logFile.getAbsolutePath());
            String fileName = logFile.toString();
            sandbox = new File(fileName.substring(0, fileName.length() - 4) + ".d");

            // check if the similarly named directory is free
            if (!sandbox.exists()) {
                break;
            }

            //noinspection ResultOfMethodCallIgnored
            logFile.delete();
        }
        if (!sandbox.mkdir()) {
            throw new RuntimeException("Failed to create temp dir: " + sandbox);
        }

        sandbox = sandbox.getCanonicalFile();
        logFile = logFile.getCanonicalFile();

        try {
            jarFile = findJarFile(method.getDeclaringClass()).getCanonicalFile();
        } catch (Throwable throwable) {
            throw new IllegalStateException("SelfModifier must be in a jar file");
        }
        log("JarFile: " + jarFile);

        extractJarFile();

        if (args == null) {
            args = new String[0];
        }
        spawn(args, 2);

        // finally, if all went well, the invoking process must exit
        log("Exit");
        System.exit(0);
    }

    /**
     * Run a new jvm with all the system parameters needed for phases 2 and 3.
     *
     * @param args      the command line arguments
     * @param nextPhase the next phase
     * @return the spawned process
     * @throws IOException if there is an error getting the canonical name of a path
     */
    private Process spawn(String[] args, int nextPhase) throws IOException {
        String base = logFile.getAbsolutePath();
        base = base.substring(0, base.length() - 4);

        // invoke from tmpdir, passing target method arguments as args, and
        // SelfModifier parameters as system properties
        String javaCommand = ProcessHelper.getJavaCommand();

        List<String> command = new ArrayList<String>();
        command.add(javaCommand);
        command.addAll(new JVMHelper().getJVMArguments());

        if (nextPhase == 2) {
            if (debugPort2 != -1) {
                command.add(getDebug(debugPort2));
            }
            if (debugPort3 != -1) {
                // propagate the phase3 debug port
                command.add("-D" + DEBUG_PORT3_KEY + "=" + debugPort3);
            }
        } else if (nextPhase == 3 && debugPort3 != -1) {
            command.add(getDebug(debugPort3));
        }

        command.add("-classpath");
        command.add(sandbox.getAbsolutePath());
        command.add("-D" + BASE_KEY + "=" + base);
        command.add("-D" + JAR_KEY + "=" + jarFile.getPath() + "");
        command.add("-D" + CLASS_KEY + "=" + method.getDeclaringClass().getName());
        command.add("-D" + METHOD_KEY + "=" + method.getName());
        command.add("-D" + PHASE_KEY + "=" + nextPhase);
        command.add(getClass().getName());

        Collections.addAll(command, args);

        StringBuilder buffer = new StringBuilder("Spawning phase ");
        buffer.append(nextPhase).append(": ");
        for (String anEntireCmd : command) {
            buffer.append("\n\t").append(anEntireCmd);
        }
        log(buffer.toString());

        return ProcessHelper.exec(command);
    }

    /**
     * Retrieve the jar file the specified class was loaded from.
     *
     * @return null if file was not loaded from a jar file
     * @throws SecurityException if access to is denied by SecurityManager
     */
    public static File findJarFile(Class<?> clazz) {
        String resource = clazz.getName().replace('.', '/') + ".class";

        URL url = ClassLoader.getSystemResource(resource);
        if (!"jar".equals(url.getProtocol())) {
            return null;
        }

        String path = url.getFile();
        // starts at "file:..." (use getPath() as of 1.3)
        path = path.substring(0, path.lastIndexOf('!'));

        File file;

        // getSystemResource() returns a valid URL (eg. spaces are %20), but a
        // file
        // Constructed w/ it will expect "%20" in path. URI and File(URI)
        // properly
        file = new File(URI.create(path));

        return file;
    }

    /**
     * @throws IOException if an error occured
     */
    private void extractJarFile() throws IOException {
        int extracted = 0;
        InputStream in = null;
        String MANIFEST = "META-INF/MANIFEST.MF";
        JarFile jar = new JarFile(jarFile, true);

        try {
            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                if (entry.isDirectory()) {
                    continue;
                }

                String pathname = entry.getName();
                if (MANIFEST.equals(pathname.toUpperCase())) {
                    continue;
                }

                in = jar.getInputStream(entry);
                FileUtils.copyToFile(in, new File(sandbox, pathname));

                extracted++;
            }
            log("Extracted " + extracted + " file" + (extracted > 1 ? "s" : "") + " into " + sandbox.getPath());
        } finally {
            try {
                jar.close();
            } catch (IOException ignore) {
            }
            IOUtils.closeQuietly(in);
        }
    }

    /**
     * Invoke phase 2, which starts phase 3, then cleans up the sandbox. This is needed because
     * GUI's often call the exit() method to kill the AWT thread, and early versions of java did not
     * have exit hooks. In order to delete the sandbox on exit we invoke method in separate process
     * and wait for that process to complete. Even worse, resources in the jar may be locked by the
     * target process, which would prevent the sandbox from being deleted as well.
     */
    private void invoke2(String[] args) {

        int retVal = -1;
        try {
            // TODO: in jre 1.2, Phs1 consistently needs more time to unlock the
            // original jar. Phs2 should wait to invoke Phs3 until it knows its
            // parent (Phs1) has died, but Process.waitFor() only works on
            // children. Can we see when a parent dies, or /this/ Process
            // becomes
            // orphaned?
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ignored) {
            }

            // spawn phase 3, capture its stdio and wait for it to exit
            Process process = spawn(args, 3);

            try {
                retVal = process.waitFor();
            } catch (InterruptedException e) {
                log(e);
            }

            // clean up and go
            log("deleting sandbox");
            FileUtils.deleteDirectory(sandbox);
        } catch (Exception e) {
            log(e);
        }
        log("Phase 3 return value = " + retVal);
    }

    /**
     * Invoke the target method and let it run free!
     */
    private void invoke3(String[] args) {
        // std io is being redirected to the log
        try {
            errlog("Invoking method: " + method.getDeclaringClass().getName() + "." + method.getName()
                    + "(String[] args)");

            method.invoke(null, new Object[] { args });
        } catch (Throwable t) {
            errlog(t.getMessage());
            t.printStackTrace();
            errlog("exiting");
            System.err.flush();
            System.exit(31);
        }

        errlog("Method returned, waiting for other threads");
        System.err.flush();
        // now let the method call exit...
    }

    /**
     * ********************************************************************************************
     * --------------------------------------------------------------------- Logging
     * ---------------------------------------------------------------------
     */

    private PrintStream log = null;

    private void errlog(String msg) {
        date.setTime(System.currentTimeMillis());
        System.err.println(isoPoint.format(date) + " Phase " + phase + ": " + msg);
    }

    private PrintStream checkLog() {
        try {
            if (log == null) {
                log = new PrintStream(new FileOutputStream(logFile.toString(), true));
            }
        } catch (IOException x) {
            System.err.println("Phase " + phase + " log err: " + x.getMessage());
            x.printStackTrace();
        }
        date.setTime(System.currentTimeMillis());
        return log;
    }

    private void log(Throwable t) {
        if (checkLog() != null) {
            log.println(isoPoint.format(date) + " Phase " + phase + ": " + t.getMessage());
            t.printStackTrace(log);
        }
    }

    private void log(String msg) {
        if (checkLog() != null) {
            log.println(isoPoint.format(date) + " Phase " + phase + ": " + msg);
        }
    }

    /**
     * Returns the command to enable remote debugging.
     *
     * @param port the port to listen on
     * @return the command
     */
    private String getDebug(int port) {
        return "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=" + port;
    }

}