001: /*
002: * $Id:$
003: * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
004: *
005: * http://izpack.org/ http://izpack.codehaus.org/
006: *
007: * Copyright 2006 Klaus Bartz
008: *
009: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
010: * in compliance with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing, software distributed under the License
015: * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
016: * or implied. See the License for the specific language governing permissions and limitations under
017: * the License.
018: */
019: package com.izforge.izpack.util;
020:
021: import java.util.List;
022: import java.io.BufferedOutputStream;
023: import java.io.BufferedReader;
024: import java.io.BufferedWriter;
025: import java.io.File;
026: import java.io.FileInputStream;
027: import java.io.FileOutputStream;
028: import java.io.IOException;
029: import java.io.InputStream;
030: import java.io.InputStreamReader;
031: import java.io.OutputStream;
032: import java.io.OutputStreamWriter;
033: import java.io.PrintStream;
034: import java.io.PrintWriter;
035: import java.text.SimpleDateFormat;
036: import java.util.ArrayList;
037: import java.util.Collections;
038: import java.util.Date;
039: import java.util.Iterator;
040: import java.util.TreeSet;
041:
042: /**
043: * This class tries to remove a given list of files which are locked by this process. For this the
044: * paths of the files are stored in a temporary file and a new process will be created. The class
045: * files which are needed by the new process will be unpacked from the jar file under users temp dir
046: * in a "sandbox". The new process receive the path of the temporary file and some other
047: * information. After a wait intervall it reads the path file and removes all files which there are
048: * listed. Next the created "sandbox" and the path file will be removed. This class uses the
049: * characteristik of the system loader that jar files will be keeped open, simple class files will
050: * be closed after loading a class. Therefore jar files are locked and cannot be deleted, class
051: * files are not locked and deletable.<br>
052: * The idea for this stuff is copied from Chadwick McHenry's SelfModifier in the uninstaller stuff
053: * of IzPack.
054: *
055: * @author Klaus Bartz
056: *
057: */
058: public class LibraryRemover {
059:
060: /**
061: * All class files which are needed for the second process. All have to be in this installers
062: * jar file. No slash in front should be used; no dot else slashes should be used;
063: * extension (.class) will be required.
064: */
065: private static final String[] SANDBOX_CONTENT = { "com/izforge/izpack/util/LibraryRemover.class" };
066:
067: /** System property name of base for log and sandbox of secondary processes. */
068: private static final String BASE_KEY = "lib.rem.base";
069:
070: /** System property name of phase (1, 2, or 3) indicator. */
071: private static final String PHASE_KEY = "self.mod.phase";
072:
073: /** VM home Needed for the java command. */
074: private static final String JAVA_HOME = System
075: .getProperty("java.home");
076:
077: /** Prefix of sandbox, path and log file. */
078: private static final String PREFIX = "InstallRemover";
079:
080: /** Phase of this process. */
081: private int phase = 0;
082:
083: /** Log for phase 2, because we can't capture the stdio from them. */
084: private File logFile = null;
085:
086: /** Directory which we extract too, invoke from, and finally delete. */
087: private File sandbox = null;
088:
089: /** The file which contains the paths of the files to delete. */
090: private File specFile = null;
091:
092: /** For logging time. */
093: private SimpleDateFormat isoPoint = new SimpleDateFormat(
094: "yyyy-MM-dd'T'HH:mm:ss.SSS");
095:
096: /** Also for logging time. */
097: private Date date = new Date();
098:
099: /**
100: * Constructor for both phases. Depending on the phase different initializing will be performed.
101: *
102: * @param phase for which an object should be created.
103: * @throws IOException
104: */
105: private LibraryRemover(int phase) throws IOException {
106: this .phase = phase;
107: if (phase == 1) {
108: initJavaExec();
109: } else {
110: logFile = new File(System.getProperty(BASE_KEY) + ".log");
111: specFile = new File(System.getProperty(BASE_KEY) + ".spec");
112: sandbox = new File(System.getProperty(BASE_KEY) + ".d");
113: }
114: }
115:
116: /**
117: * Entry point for phase 1. This class tries to remove all files given in the Vector.
118: *
119: * @param temporaryFileNames
120: * @throws IOException
121: */
122: public static void invoke(List<String> temporaryFileNames)
123: throws IOException {
124: LibraryRemover self = new LibraryRemover(1);
125: self.invoke1(temporaryFileNames);
126: }
127:
128: /**
129: * This call ensures that java can be exec'd in a separate process.
130: *
131: * @throws IOException if an I/O error occurs, indicating java is unable to be exec'd
132: * @throws SecurityException if a security manager exists and doesn't allow creation of a
133: * subprocess
134: */
135: private void initJavaExec() throws IOException {
136: try {
137: Process p = Runtime.getRuntime().exec(javaCommand());
138:
139: new StreamProxy(p.getErrorStream(), "err").start();
140: new StreamProxy(p.getInputStream(), "out").start();
141: p.getOutputStream().close();
142:
143: // even if it returns an error code, it was at least found
144: p.waitFor();
145: } catch (InterruptedException ie) {
146: throw new IOException("Unable to create a java subprocess");
147: }
148: }
149:
150: /**
151: * Internal invoke method for phase 1.
152: *
153: * @param temporaryFileNames list of paths of the files which should be removed
154: * @throws IOException
155: */
156: private void invoke1(List<String> temporaryFileNames)
157: throws IOException {
158: // Initialize sandbox and log file to be unique, but similarly named
159: while (true) {
160: logFile = File.createTempFile(PREFIX, ".log");
161: String f = logFile.getCanonicalPath();
162: specFile = new File(f.substring(0, f.length() - 4)
163: + ".spec");
164: sandbox = new File(f.substring(0, f.length() - 4) + ".d");
165:
166: // check if the similarly named directory is free
167: if (!sandbox.exists())
168: break;
169:
170: logFile.delete();
171: }
172: if (!sandbox.mkdir())
173: throw new RuntimeException("Failed to create temp dir: "
174: + sandbox);
175:
176: sandbox = sandbox.getCanonicalFile();
177: logFile = logFile.getCanonicalFile();
178: OutputStream out = null;
179: InputStream in = null;
180: byte[] buf = new byte[5120];
181: int extracted = 0;
182: // Write out the class files from the current installer jar into the sandbox.
183: // This allows later to delete the classes because class files are deleteable
184: // also the using process is running, jar files are not deletable in that
185: // situation.,
186: for (String aSANDBOX_CONTENT : SANDBOX_CONTENT) {
187: in = getClass().getResourceAsStream("/" + aSANDBOX_CONTENT);
188:
189: File outFile = new File(sandbox, aSANDBOX_CONTENT);
190: File parent = outFile.getParentFile();
191: if (parent != null && !parent.exists()) {
192: parent.mkdirs();
193: }
194:
195: out = new BufferedOutputStream(
196: new FileOutputStream(outFile));
197:
198: int n;
199: while ((n = in.read(buf, 0, buf.length)) > 0) {
200: out.write(buf, 0, n);
201: }
202:
203: out.close();
204: extracted++;
205:
206: }
207: // We write a file which contains the paths to remove.
208: out = new BufferedOutputStream(new FileOutputStream(specFile));
209: BufferedWriter specWriter = new BufferedWriter(
210: new OutputStreamWriter(out));
211: Iterator<String> iter = temporaryFileNames.iterator();
212: while (iter.hasNext()) {
213: specWriter.write(iter.next());
214: if (iter.hasNext())
215: specWriter.newLine();
216: }
217: specWriter.flush();
218: out.close();
219:
220: spawn(2);
221:
222: // finally, if all went well, the invoking process must exit
223: log("Exit");
224: System.exit(0);
225: }
226:
227: /**
228: * Returns an ArrayList of the files to delete.
229: *
230: * @return The files list.
231: * @exception Exception Description of the Exception
232: */
233: private ArrayList<File> getFilesList() throws Exception {
234: // Initialisations
235: TreeSet<File> files = new TreeSet<File>(Collections
236: .reverseOrder());
237: InputStream in = new FileInputStream(specFile);
238: InputStreamReader inReader = new InputStreamReader(in);
239: BufferedReader reader = new BufferedReader(inReader);
240:
241: // We read it
242: String read = reader.readLine();
243: while (read != null) {
244: files.add(new File(read));
245: read = reader.readLine();
246: }
247: in.close();
248: // We return it
249: return new ArrayList<File>(files);
250: }
251:
252: /**
253: * Invoke methode for phase 2.
254: */
255: private void invoke2() {
256:
257: try {
258: // Give main program time to exit.
259: try {
260: Thread.sleep(1000);
261: } catch (Exception x) {
262: }
263:
264: ArrayList<File> files = getFilesList();
265: int size = files.size();
266: // We destroy the files
267:
268: log("deleteing temporary dlls/shls");
269: for (int i = 0; i < size; i++) {
270: File file = files.get(i);
271: file.delete();
272: if (file.exists())
273: log(" deleting of " + file.getCanonicalPath()
274: + " failed!!!");
275: else
276: log(" " + file.getCanonicalPath());
277:
278: }
279:
280: // clean up and go
281: log("deleteing sandbox");
282: deleteTree(sandbox);
283: specFile.delete();
284: } catch (Exception e) {
285: log(e);
286: }
287: }
288:
289: /**
290: * Copied from com.izforge.izpack.uninstaller.SelfModifier. Little addaption for this class.
291: *
292: * @param nextPhase phase of the spawn
293: * @return created process object
294: * @throws IOException
295: */
296: private Process spawn(int nextPhase) throws IOException {
297: String base = logFile.getAbsolutePath();
298: base = base.substring(0, base.length() - 4);
299:
300: // invoke from tmpdir, passing target method arguments as args, and
301: // SelfModifier parameters as sustem properties
302: String[] javaCmd = new String[] { javaCommand(), "-classpath",
303: sandbox.getAbsolutePath(),
304: "-D" + BASE_KEY + "=" + base,
305: "-D" + PHASE_KEY + "=" + nextPhase,
306: getClass().getName() };
307:
308: StringBuffer sb = new StringBuffer("Spawning phase ");
309: sb.append(nextPhase).append(": ");
310: for (String aJavaCmd : javaCmd) {
311: sb.append("\n\t").append(aJavaCmd);
312: }
313: log(sb.toString());
314:
315: // Just invoke it and let it go, the exception will be caught above
316: return Runtime.getRuntime().exec(javaCmd, null, null); // workDir);
317: }
318:
319: /**
320: * Recursively delete a file structure. Copied from com.izforge.izpack.uninstaller.SelfModifier.
321: * Little addaption to this class.
322: *
323: * @return command for spawning
324: */
325: public static boolean deleteTree(File file) {
326: if (file.isDirectory()) {
327: File[] files = file.listFiles();
328: for (File file1 : files) {
329: deleteTree(file1);
330: }
331: }
332: return file.delete();
333: }
334:
335: /**
336: * Copied from com.izforge.izpack.uninstaller.SelfModifier.
337: *
338: * @return command command extended with extension of executable
339: */
340: private static String addExtension(String command) {
341: // This is the most common extension case - exe for windows and OS/2,
342: // nothing for *nix.
343: return command
344: + (OsVersion.IS_WINDOWS || OsVersion.IS_OS2 ? ".exe"
345: : "");
346: }
347:
348: /**
349: * Copied from com.izforge.izpack.uninstaller.SelfModifier. Little addaption for this class.
350: *
351: * @return command for spawning
352: */
353: private static String javaCommand() {
354: String executable = addExtension("java");
355: String dir = new File(JAVA_HOME + "/bin").getAbsolutePath();
356: File jExecutable = new File(dir, executable);
357:
358: // Unfortunately on Windows java.home doesn't always refer
359: // to the correct location, so we need to fall back to
360: // assuming java is somewhere on the PATH.
361: if (!jExecutable.exists())
362: return executable;
363: return jExecutable.getAbsolutePath();
364: }
365:
366: public static void main(String[] args) {
367: // Phase 2 removes given path list, sandbox and spec file.
368: // Phase 3 as used in SelfModifier will be not needed here because
369: // this class do not use a GUI which can call exit like the
370: // one in SelfModifier.
371:
372: try {
373: // all it's attributes are retrieved from system properties
374: LibraryRemover librianRemover = new LibraryRemover(2);
375: librianRemover.invoke2();
376: } catch (IOException ioe) {
377: System.err.println("Error invoking a secondary phase");
378: System.err
379: .println("Note that this program is only intended as a secondary process");
380: ioe.printStackTrace();
381: }
382: }
383:
384: /***********************************************************************************************
385: * --------------------------------------------------------------------- Logging
386: * --------------------------------------------------------------------- Copied from
387: * com.izforge.izpack.uninstaller.SelfModifier.
388: */
389:
390: PrintStream log = null;
391:
392: private PrintStream checkLog() {
393: try {
394: if (log == null)
395: log = new PrintStream(new FileOutputStream(logFile
396: .toString(), true));
397: } catch (IOException x) {
398: System.err.println("Phase " + phase + " log err: "
399: + x.getMessage());
400: x.printStackTrace();
401: }
402: date.setTime(System.currentTimeMillis());
403: return log;
404: }
405:
406: private void log(Throwable t) {
407: if (checkLog() != null) {
408: log.println(isoPoint.format(date) + " Phase " + phase
409: + ": " + t.getMessage());
410: t.printStackTrace(log);
411: }
412: }
413:
414: private void log(String msg) {
415: if (checkLog() != null)
416: log.println(isoPoint.format(date) + " Phase " + phase
417: + ": " + msg);
418: }
419:
420: public static class StreamProxy extends Thread {
421:
422: InputStream in;
423:
424: String name;
425:
426: OutputStream out;
427:
428: public StreamProxy(InputStream in, String name) {
429: this (in, name, null);
430: }
431:
432: public StreamProxy(InputStream in, String name, OutputStream out) {
433: this .in = in;
434: this .name = name;
435: this .out = out;
436: }
437:
438: public void run() {
439: try {
440: PrintWriter pw = null;
441: if (out != null)
442: pw = new PrintWriter(out);
443:
444: BufferedReader br = new BufferedReader(
445: new InputStreamReader(in));
446: String line;
447: while ((line = br.readLine()) != null) {
448: if (pw != null)
449: pw.println(line);
450: // System.out.println(name + ">" + line);
451: }
452: if (pw != null)
453: pw.flush();
454: } catch (IOException ioe) {
455: ioe.printStackTrace();
456: }
457: }
458: }
459:
460: }
|