All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.izforge.izpack.util.SelfModifier Maven / Gradle / Ivy

There is a newer version: 5.2.3
Show newest version
/*
 * 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.
 * 

*

* 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. *

*

* If the constructor or invoke() methods fail, it is generally because secondary java processes * could not be started. *

* Requirements *

    *
  • The target method, and all it's required classes must be in a jar file. *
  • The Self Modifier, and its inner classes must also be in the jar file. *
*

* There are three system processes (or "phases") involved, the first invoked by the user, the * second and third by the SelfModifier. *

*

* Phase 1: *

    *
  1. Program is launched, SelfModifier is created, invoke(String[]) is called *
  2. A temporary directory (or "sandbox") is created in the default temp directory, and the jar * file contents ar extracted into it *
  3. 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 SelfModifier system properties set. *
  4. Immediately exit so the system unlocks the jar file *
*

* Phase 2: *

    *
  1. Initializes from system properties. *
  2. Spawn phase 3 exactly as phase 2 except the self.modifier.phase system properties set to 3. *
  3. Wait for phase 3 to die *
  4. Delete the temporary sandbox *
*

* Phase 3: *

    *
  1. Initializes from system properties. *
  2. Redirect std err stream to the log *
  3. Invoke the target method with arguments we were given *
  4. 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. *
*

* SelfModifier system properties used to pass information * between processes.

* * * * * * * * * * * * *
Constant * System property * description
BASE_KEY * self.mod.jar * base path to log file and sandbox dir
JAR_KEY * self.mod.class * path to original jar file
CLASS_KEY * self.mod.method * class of target method
METHOD_KEY * self.mod.phase * name of method to be invoked in sandbox
PHASE_KEY * self.mod.base * phase of operation to run
* * @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 -1 if not set or invalid */ private final int debugPort2 = Integer.getInteger(DEBUG_PORT2_KEY, -1); /** * Debug port for phase 3, or -1 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. *

* 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 method is null * @throws IllegalArgumentException if method 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); } @Deprecated public SelfModifier(Method method, long maxmemory, long maxpermgensize) throws IOException { this(method); } /** * Check the method for the required properties (public, static, params:(String[])). * * @param method the method * @throws NullPointerException if method is null * @throws IllegalArgumentException if method 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)}. *

*

* 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}. *

* * @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 command = new ArrayList(); 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 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; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy