/* * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved. * * http://izpack.org/ * http://izpack.codehaus.org/ * * Copyright 2007 Dennis Reil * * 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.installer; import com.izforge.izpack.LocaleDatabase; import com.izforge.izpack.Pack; import com.izforge.izpack.PackFile; import com.izforge.izpack.UpdateCheck; import com.izforge.izpack.event.InstallerListener; import com.izforge.izpack.rules.RulesEngine; import com.izforge.izpack.util.AbstractUIProgressHandler; import com.izforge.izpack.util.Debug; import com.izforge.izpack.util.IoHelper; import com.izforge.izpack.util.VariableSubstitutor; import org.apache.regexp.RE; import org.apache.regexp.RECompiler; import org.apache.regexp.RESyntaxException; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; /** * Abstract base class for all unpacker implementations. * * @author Dennis Reil, <izpack@reil-online.de> */ public abstract class UnpackerBase implements IUnpacker { /** * The installdata. */ protected AutomatedInstallData idata; /** * The installer listener. */ protected AbstractUIProgressHandler handler; /** * The uninstallation data. */ protected UninstallData udata; /** * The variables substitutor. */ protected VariableSubstitutor vs; /** * The absolute path of the installation. (NOT the canonical!) */ protected File absolute_installpath; /** * The absolute path of the source installation jar. */ private File absolutInstallSource; /** * The packs locale database. */ protected LocaleDatabase langpack = null; /** * The result of the operation. */ protected boolean result = true; /** * The instances of the unpacker objects. */ protected static HashMap<Object, String> instances = new HashMap<Object, String>(); /** * Interrupt flag if global interrupt is desired. */ protected static boolean interruptDesired = false; /** * Do not perform a interrupt call. */ protected static boolean discardInterrupt = false; /** * The name of the XML file that specifies the panel langpack */ protected static final String LANG_FILE_NAME = "packsLang.xml"; public static final String ALIVE = "alive"; public static final String INTERRUPT = "doInterrupt"; public static final String INTERRUPTED = "interruppted"; protected RulesEngine rules; /** * The constructor. * * @param idata The installation data. * @param handler The installation progress handler. */ public UnpackerBase(AutomatedInstallData idata, AbstractUIProgressHandler handler) { try { String resource = LANG_FILE_NAME + "_" + idata.localeISO3; this.langpack = new LocaleDatabase(ResourceManager.getInstance().getInputStream(resource)); } catch (Throwable exception) { } this.idata = idata; this.handler = handler; // Initialize the variable substitutor vs = new VariableSubstitutor(idata.getVariables()); } public void setRules(RulesEngine rules) { this.rules = rules; } /** * Returns a copy of the active unpacker instances. * * @return a copy of active unpacker instances */ public static HashMap getRunningInstances() { synchronized (instances) { // Return a shallow copy to prevent a // ConcurrentModificationException. return (HashMap) (instances.clone()); } } /** * Adds this to the map of all existent instances of Unpacker. */ protected void addToInstances() { synchronized (instances) { instances.put(this, ALIVE); } } /** * Removes this from the map of all existent instances of Unpacker. */ protected void removeFromInstances() { synchronized (instances) { instances.remove(this); } } /** * Initiate interrupt of all alive Unpacker. This method does not interrupt the Unpacker objects * else it sets only the interrupt flag for the Unpacker objects. The dispatching of interrupt * will be performed by the Unpacker objects self. */ private static void setInterruptAll() { synchronized (instances) { Iterator iter = instances.keySet().iterator(); while (iter.hasNext()) { Object key = iter.next(); if (instances.get(key).equals(ALIVE)) { instances.put(key, INTERRUPT); } } // Set global flag to allow detection of it in other classes. // Do not set it to thread because an exec will then be stoped. setInterruptDesired(true); } } /** * Initiate interrupt of all alive Unpacker and waits until all Unpacker are interrupted or the * wait time has arrived. If the doNotInterrupt flag in InstallerListener is set to true, the * interrupt will be discarded. * * @param waitTime wait time in millisecounds * @return true if the interrupt will be performed, false if the interrupt will be discarded */ public static boolean interruptAll(long waitTime) { long t0 = System.currentTimeMillis(); if (isDiscardInterrupt()) { return (false); } setInterruptAll(); while (!isInterruptReady()) { if (System.currentTimeMillis() - t0 > waitTime) { return (true); } try { Thread.sleep(100); } catch (InterruptedException e) { } } return (true); } private static boolean isInterruptReady() { synchronized (instances) { Iterator iter = instances.keySet().iterator(); while (iter.hasNext()) { Object key = iter.next(); if (!instances.get(key).equals(INTERRUPTED)) { return (false); } } return (true); } } /** * Sets the interrupt flag for this Unpacker to INTERRUPTED if the previos state was INTERRUPT * or INTERRUPTED and returns whether interrupt was initiate or not. * * @return whether interrupt was initiate or not */ protected boolean performInterrupted() { synchronized (instances) { Object doIt = instances.get(this); if (doIt != null && (doIt.equals(INTERRUPT) || doIt.equals(INTERRUPTED))) { instances.put(this, INTERRUPTED); this.result = false; return (true); } return (false); } } /** * Returns whether interrupt was initiate or not for this Unpacker. * * @return whether interrupt was initiate or not */ private boolean shouldInterrupt() { synchronized (instances) { Object doIt = instances.get(this); if (doIt != null && (doIt.equals(INTERRUPT) || doIt.equals(INTERRUPTED))) { return (true); } return (false); } } /** * Return the state of the operation. * * @return true if the operation was successful, false otherwise. */ public boolean getResult() { return this.result; } /** * @param filename * @param patterns * @return true if the file matched one pattern, false if it did not */ private boolean fileMatchesOnePattern(String filename, ArrayList<RE> patterns) { // first check whether any include matches for (RE pattern : patterns) { if (pattern.match(filename)) { return true; } } return false; } /** * @param list A list of file name patterns (in ant fileset syntax) * @param recompiler The regular expression compiler (used to speed up RE compiling). * @return List of org.apache.regexp.RE */ private List<RE> preparePatterns(ArrayList<String> list, RECompiler recompiler) { ArrayList<RE> result = new ArrayList<RE>(); for (String element : list) { if ((element != null) && (element.length() > 0)) { // substitute variables in the pattern element = this.vs.substitute(element, "plain"); // check whether the pattern is absolute or relative File f = new File(element); // if it is relative, make it absolute and prepend the // installation path // (this is a bit dangerous...) if (!f.isAbsolute()) { element = new File(this.absolute_installpath, element).toString(); } // now parse the element and construct a regular expression from // it // (we have to parse it one character after the next because // every // character should only be processed once - it's not possible // to get this // correct using regular expression replacing) StringBuffer element_re = new StringBuffer(); int lookahead = -1; int pos = 0; while (pos < element.length()) { char c; if (lookahead != -1) { c = (char) lookahead; lookahead = -1; } else { c = element.charAt(pos++); } switch (c) { case '/': { element_re.append(File.separator); break; } // escape backslash and dot case '\\': case '.': { element_re.append("\\"); element_re.append(c); break; } case '*': { if (pos == element.length()) { element_re.append("[^").append(File.separator).append("]*"); break; } lookahead = element.charAt(pos++); // check for "**" if (lookahead == '*') { element_re.append(".*"); // consume second star lookahead = -1; } else { element_re.append("[^").append(File.separator).append("]*"); // lookahead stays there } break; } default: { element_re.append(c); break; } } // switch } // make sure that the whole expression is matched element_re.append('$'); // replace \ by \\ and create a RE from the result try { result.add(new RE(recompiler.compile(element_re.toString()))); } catch (RESyntaxException e) { this.handler.emitNotification("internal error: pattern \"" + element + "\" produced invalid RE \"" + f.getPath() + "\""); } } } return result; } // CUSTOM ACTION STUFF -------------- start ----------------- /** * Informs all listeners which would be informed at the given action type. * * @param customActions array of lists with the custom action objects * @param action identifier for which callback should be called * @param firstParam first parameter for the call * @param secondParam second parameter for the call * @param thirdParam third parameter for the call */ protected void informListeners(List[] customActions, int action, Object firstParam, Object secondParam, Object thirdParam) throws Exception { List listener = null; // select the right action list. switch (action) { case InstallerListener.BEFORE_FILE: case InstallerListener.AFTER_FILE: case InstallerListener.BEFORE_DIR: case InstallerListener.AFTER_DIR: listener = customActions[customActions.length - 1]; break; default: listener = customActions[0]; break; } if (listener == null) { return; } // Iterate the action list. Iterator iter = listener.iterator(); while (iter.hasNext()) { if (shouldInterrupt()) { return; } InstallerListener il = (InstallerListener) iter.next(); switch (action) { case InstallerListener.BEFORE_FILE: il.beforeFile((File) firstParam, (PackFile) secondParam); break; case InstallerListener.AFTER_FILE: il.afterFile((File) firstParam, (PackFile) secondParam); break; case InstallerListener.BEFORE_DIR: il.beforeDir((File) firstParam, (PackFile) secondParam); break; case InstallerListener.AFTER_DIR: il.afterDir((File) firstParam, (PackFile) secondParam); break; case InstallerListener.BEFORE_PACK: il.beforePack((Pack) firstParam, (Integer) secondParam, (AbstractUIProgressHandler) thirdParam); break; case InstallerListener.AFTER_PACK: il.afterPack((Pack) firstParam, (Integer) secondParam, (AbstractUIProgressHandler) thirdParam); break; case InstallerListener.BEFORE_PACKS: il.beforePacks((AutomatedInstallData) firstParam, (Integer) secondParam, (AbstractUIProgressHandler) thirdParam); break; case InstallerListener.AFTER_PACKS: il.afterPacks((AutomatedInstallData) firstParam, (AbstractUIProgressHandler) secondParam); break; } } } /** * Returns the defined custom actions split into types including a constructed type for the file * related installer listeners. * * @return array of lists of custom action data like listeners */ protected List[] getCustomActions() { String[] listenerNames = AutomatedInstallData.CUSTOM_ACTION_TYPES; List[] retval = new List[listenerNames.length + 1]; int i; for (i = 0; i < listenerNames.length; ++i) { retval[i] = idata.customData.get(listenerNames[i]); if (retval[i] == null) // Make a dummy list, then iterator is ever callable. { retval[i] = new ArrayList(); } } if (retval[AutomatedInstallData.INSTALLER_LISTENER_INDEX].size() > 0) { // Installer listeners exist // Create file related installer listener list in the last // element of custom action array. i = retval.length - 1; // Should be so, but safe is safe ... retval[i] = new ArrayList(); Iterator iter = retval[AutomatedInstallData.INSTALLER_LISTENER_INDEX] .iterator(); while (iter.hasNext()) { // If we get a class cast exception many is wrong and // we must fix it. InstallerListener li = (InstallerListener) iter.next(); if (li.isFileListener()) { retval[i].add(li); } } } return (retval); } // This method is only used if a file related custom action exist. /** * Creates the given directory recursive and calls the method "afterDir" of each listener with * the current file object and the pack file object. On error an exception is raised. * * @param dest the directory which should be created * @param pf current pack file object * @param customActions all defined custom actions * @return false on error, true else * @throws Exception */ protected boolean mkDirsWithEnhancement(File dest, PackFile pf, List[] customActions) throws Exception { String path = "unknown"; if (dest != null) { path = dest.getAbsolutePath(); } if (dest != null && !dest.exists() && dest.getParentFile() != null) { if (dest.getParentFile().exists()) { informListeners(customActions, InstallerListener.BEFORE_DIR, dest, pf, null); } if (!dest.mkdir()) { mkDirsWithEnhancement(dest.getParentFile(), pf, customActions); if (!dest.mkdir()) { dest = null; } } informListeners(customActions, InstallerListener.AFTER_DIR, dest, pf, null); } if (dest == null) { handler.emitError("Error creating directories", "Could not create directory\n" + path); handler.stopAction(); return (false); } return (true); } // CUSTOM ACTION STUFF -------------- end ----------------- /** * Returns whether an interrupt request should be discarded or not. * * @return Returns the discard interrupt flag */ public static synchronized boolean isDiscardInterrupt() { return discardInterrupt; } /** * Sets the discard interrupt flag. * * @param di the discard interrupt flag to set */ public static synchronized void setDiscardInterrupt(boolean di) { discardInterrupt = di; setInterruptDesired(false); } /** * Returns the interrupt desired state. * * @return the interrupt desired state */ public static boolean isInterruptDesired() { return interruptDesired; } /** * @param interruptDesired The interrupt desired flag to set */ private static void setInterruptDesired(boolean interruptDesired) { UnpackerBase.interruptDesired = interruptDesired; } /** * Puts the uninstaller. * * @throws Exception Description of the Exception */ protected void putUninstaller() throws Exception { String uninstallerCondition = idata.info.getUninstallerCondition(); if ((uninstallerCondition != null) && (uninstallerCondition.length() > 0) && !this.rules.isConditionTrue(uninstallerCondition)){ Debug.log("Uninstaller has a condition (" + uninstallerCondition + ") which is not fulfilled."); Debug.log("Skipping creation of uninstaller."); return; } // get the uninstaller base, returning if not found so that // idata.uninstallOutJar remains null InputStream[] in = new InputStream[2]; in[0] = UnpackerBase.class.getResourceAsStream("/res/IzPack.uninstaller"); if (in[0] == null) { return; } // The uninstaller extension is facultative; it will be exist only // if a native library was marked for uninstallation. in[1] = UnpackerBase.class.getResourceAsStream("/res/IzPack.uninstaller-ext"); // Me make the .uninstaller directory String dest = IoHelper.translatePath(idata.info.getUninstallerPath(), vs); String jar = dest + File.separator + idata.info.getUninstallerName(); File pathMaker = new File(dest); pathMaker.mkdirs(); // We log the uninstaller deletion information udata.setUninstallerJarFilename(jar); udata.setUninstallerPath(dest); // We open our final jar file FileOutputStream out = new FileOutputStream(jar); // Intersect a buffer else byte for byte will be written to the file. BufferedOutputStream bos = new BufferedOutputStream(out); ZipOutputStream outJar = new ZipOutputStream(bos); idata.uninstallOutJar = outJar; outJar.setLevel(9); udata.addFile(jar, true); // We copy the uninstallers HashSet<String> doubles = new HashSet<String>(); for (InputStream anIn : in) { if (anIn == null) { continue; } ZipInputStream inRes = new ZipInputStream(anIn); ZipEntry zentry = inRes.getNextEntry(); while (zentry != null) { // Puts a new entry, but not twice like META-INF if (!doubles.contains(zentry.getName())) { doubles.add(zentry.getName()); outJar.putNextEntry(new ZipEntry(zentry.getName())); // Byte to byte copy int unc = inRes.read(); while (unc != -1) { outJar.write(unc); unc = inRes.read(); } // Next one please inRes.closeEntry(); outJar.closeEntry(); } zentry = inRes.getNextEntry(); } inRes.close(); } // Should we relaunch with privileges? if (idata.info.isPrivilegedExecutionRequired()) { outJar.putNextEntry(new ZipEntry("exec-admin")); outJar.closeEntry(); } // We put the langpack InputStream in2 = Unpacker.class.getResourceAsStream("/langpacks/" + idata.localeISO3 + ".xml"); outJar.putNextEntry(new ZipEntry("langpack.xml")); int read = in2.read(); while (read != -1) { outJar.write(read); read = in2.read(); } outJar.closeEntry(); } /** * Adds additional unistall data to the uninstall data object. * * @param udata unistall data * @param customData array of lists of custom action data like uninstaller listeners */ protected void handleAdditionalUninstallData(UninstallData udata, List[] customData) { // Handle uninstall libs udata.addAdditionalData("__uninstallLibs__", customData[AutomatedInstallData.UNINSTALLER_LIBS_INDEX]); // Handle uninstaller listeners udata.addAdditionalData("uninstallerListeners", customData[AutomatedInstallData.UNINSTALLER_LISTENER_INDEX]); // Handle uninstaller jars udata.addAdditionalData("uninstallerJars", customData[AutomatedInstallData.UNINSTALLER_JARS_INDEX]); } public abstract void run(); /** * @param updatechecks */ protected void performUpdateChecks(ArrayList<UpdateCheck> updatechecks) { ArrayList<RE> include_patterns = new ArrayList<RE>(); ArrayList<RE> exclude_patterns = new ArrayList<RE>(); RECompiler recompiler = new RECompiler(); this.absolute_installpath = new File(idata.getInstallPath()).getAbsoluteFile(); // at first, collect all patterns for (UpdateCheck uc : updatechecks) { if (uc.includesList != null) { include_patterns.addAll(preparePatterns(uc.includesList, recompiler)); } if (uc.excludesList != null) { exclude_patterns.addAll(preparePatterns(uc.excludesList, recompiler)); } } // do nothing if no update checks were specified if (include_patterns.size() == 0) { return; } // now collect all files in the installation directory and figure // out files to check for deletion // use a treeset for fast access TreeSet<String> installed_files = new TreeSet<String>(); for (String fname : this.udata.getInstalledFilesList()) { File f = new File(fname); if (!f.isAbsolute()) { f = new File(this.absolute_installpath, fname); } installed_files.add(f.getAbsolutePath()); } // now scan installation directory (breadth first), contains Files of // directories to scan // (note: we'll recurse infinitely if there are circular links or // similar nasty things) Stack<File> scanstack = new Stack<File>(); // contains File objects determined for deletion ArrayList<File> files_to_delete = new ArrayList<File>(); try { scanstack.add(absolute_installpath); while (!scanstack.empty()) { File f = scanstack.pop(); File[] files = f.listFiles(); if (files == null) { throw new IOException(f.getPath() + "is not a directory!"); } for (File newf : files) { String newfname = newf.getPath(); // skip files we just installed if (installed_files.contains(newfname)) { continue; } if (fileMatchesOnePattern(newfname, include_patterns) && (!fileMatchesOnePattern(newfname, exclude_patterns))) { files_to_delete.add(newf); } if (newf.isDirectory()) { scanstack.push(newf); } } } } catch (IOException e) { this.handler.emitError("error while performing update checks", e.toString()); } for (File f : files_to_delete) { if (!f.isDirectory()) // skip directories - they cannot be removed safely yet { // this.handler.emitNotification("deleting " + f.getPath()); f.delete(); } } } /** * Writes information about the installed packs and the variables at * installation time. * * @throws IOException * @throws ClassNotFoundException */ public void writeInstallationInformation() throws IOException, ClassNotFoundException { if (!idata.info.isWriteInstallationInformation()) { Debug.trace("skip writing installation information"); return; } Debug.trace("writing installation information"); String installdir = idata.getInstallPath(); List installedpacks = new ArrayList(idata.selectedPacks); File installationinfo = new File(installdir + File.separator + AutomatedInstallData.INSTALLATION_INFORMATION); if (!installationinfo.exists()) { Debug.trace("creating info file" + installationinfo.getAbsolutePath()); installationinfo.createNewFile(); } else { Debug.trace("installation information found"); // read in old information and update FileInputStream fin = new FileInputStream(installationinfo); ObjectInputStream oin = new ObjectInputStream(fin); List packs = (List) oin.readObject(); for (Object pack1 : packs) { Pack pack = (Pack) pack1; installedpacks.add(pack); } oin.close(); fin.close(); } FileOutputStream fout = new FileOutputStream(installationinfo); ObjectOutputStream oout = new ObjectOutputStream(fout); oout.writeObject(installedpacks); /* int selectedpackscount = idata.selectedPacks.size(); for (int i = 0; i < selectedpackscount; i++) { Pack pack = (Pack) idata.selectedPacks.get(i); oout.writeObject(pack); } */ oout.writeObject(idata.variables); Debug.trace("done."); oout.close(); fout.close(); } protected File getAbsolutInstallSource() throws MalformedURLException { if (absolutInstallSource == null) { URL url = getClass().getResource("/info"); if (url.getPath().startsWith("file:")) { url = new URL(url.getFile()); } absolutInstallSource = new File(url.getFile()).getAbsoluteFile().getParentFile(); if (absolutInstallSource.getAbsolutePath().endsWith("!")) { absolutInstallSource = absolutInstallSource.getParentFile(); } } return absolutInstallSource; } }