/* * $Id$ * * Copyright 2009 Glencoe Software, Inc. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package omero.util; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.FileLock; import java.util.Arrays; import java.util.List; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // TODO: Needs addressing in slf4j-friendly way // import org.apache.log4j.ConsoleAppender; // import org.apache.log4j.Level; // import org.apache.log4j.SimpleLayout; /** * Creates temporary files and folders and makes a best effort to remove them on * exit (or sooner). Typically only a single instance of this class will exist * (held in a private static field). * * @author Josh Moore, josh at glencoesoftware.com * @since 4.1 */ public class TempFileManager { private final static Logger log = LoggerFactory.getLogger(TempFileManager.class); static { // Activating logging at a static level if (System.getenv().containsKey("DEBUG")) { // TODO: Needs addressing in slf4j-friendly way //ConsoleAppender console = new ConsoleAppender(); //console.setName("System.err"); //console.setTarget(ConsoleAppender.SYSTEM_ERR); //console.setLayout(new SimpleLayout()); //console.activateOptions(); //org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger("omero"); //logger.addAppender(console); //logger.setLevel(Level.DEBUG); //logger.addAppender(console); } } /** * Global {@link TempFileManager} instance for use by the current process * and registered with the {@link Runtime#addShutdownHook(Thread)} for * cleaning up all created files on exit. Other instances can be created * for specialized purposes. */ private final static TempFileManager manager = new TempFileManager(); /** * User-accessible directory of the form $TMPDIR/omero_$USERNAME/. If the * given directory is not writable, an attempt is made to use a similar * name. */ private final File userDir; /** * Directory under which all temporary files and folders will be created. An * attempt to remove a path not in this directory will result in an * exception. */ private final File dir; /** * .lock file under {@link #dir} which is used to prevent other * {@link TempFileManager} instances (also in other languages) from cleaning * up this directory. */ private final RandomAccessFile raf; /** * Lock held on {@link #raf} */ private final FileLock lock; /** * Default constructor, passes "omero" to * {@link TempFileManager#TempFileManager(String)} */ public TempFileManager() { this("omero"); } /** * Initializes a {@link TempFileManager} instance with the user's * temporary directory containing the given prefix value. Also adds a * {@link Runtime#addShutdownHook(Thread) shutdown hook} to call * {@link #cleanup()} on exit. * @param prefix the prefix for the user's temporary directory */ public TempFileManager(String prefix) { File tmp = tmpdir(); File userDir = new File(tmp, String.format("%s_%s", prefix, username())); if (!this.create(userDir) && !this.access(userDir)) { int i = 0; while (i < 10) { File t = new File(userDir.getAbsolutePath() + "_" + i); if (this.create(t) || this.access(t)) { userDir = t; break; } } throw new RuntimeException("Failed to create temporary directory: " + userDir.getAbsolutePath()); } this.userDir = userDir; this.dir = new File(this.userDir, this.pid()); // Now create the directory. If a later step throws an // exception, we should try to rollback this change. boolean created = false; if (!this.dir.exists()) { this.dir.mkdirs(); created = true; } try { try { this.raf = new RandomAccessFile(new File(this.dir, ".lock"), "rw"); } catch (FileNotFoundException e) { throw new RuntimeException("Failed to open lock file", e); } try { lock = this.raf.getChannel().tryLock(); if (lock == null) { throw new RuntimeException("Failed to acquire lock"); } } catch (Exception e) { try { this.raf.close(); } catch (Exception e2) { log.warn("Exception on lock file close", e2); } throw new RuntimeException("Failed to lock file", e); } Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { try { cleanup(); } catch (IOException io) { log.error("Failed to cleanup TempFileManager", io); } } }); } catch (RuntimeException e) { if (created) { try { cleanup(); } catch (Exception e2) { log.warn("Error on cleanup after error", e2); } } throw e; } } /** * Releases the lock and deletes the top-level temporary directory. The lock is released * first since on some platforms like Windows the lock file cannot be * deleted even by the owner of the lock. */ protected void cleanup() throws IOException { try { if (this.lock != null) { this.lock.release(); } if (this.raf != null) { this.raf.close(); } } catch (Exception e) { log.error("Failed to release lock", e); } this.cleanTempDir(); } /** * Returns a platform-specific user-writable temporary directory. * * First, the value of "OMERO_TEMPDIR" is attempted (if available), * then user's home ("user.home") directory, then the global temp director * ("java.io.tmpdir"). * * Typical errors for any of the possible temp locations are: * <ul> * <li>non-existence</li> * <li>inability to lock</li> * </ul> * * @see <a href="http://trac.openmicroscopy.org.uk/ome/ticket/1653">ticket:1653</a> */ protected File tmpdir() { File locktest = null; String omerotmp = System.getenv().get("OMERO_TEMPDIR"); String homeprop = System.getProperty("user.home", null); String tempprop = System.getProperty("java.io.tmpdir", null); List<String> targets = Arrays.asList(omerotmp, homeprop, tempprop); for (String target : targets) { if (target == null) { continue; } RandomAccessFile raftest = null; try { File testdir = new File(target); locktest = File.createTempFile("._omero_util_TempFileManager_lock_test", ".tmp", testdir); locktest.delete(); raftest = new RandomAccessFile(locktest, "rw"); FileLock channeltest = raftest.getChannel().tryLock(); channeltest.release(); } catch (Exception e) { if ("Operation not permitted".equals(e.getMessage())|| "Operation not supported".equals(e.getMessage())) { // This is the issue described in ticket:1653 // To prevent printing the warning, we just continue // here. log.debug(target + " does not support locking."); } else { log.warn("Invalid tmp dir: "+target, e); } continue; } finally { if (locktest !=null && raftest != null) { try { raftest.close(); locktest.delete(); } catch (Exception e) { log.warn("Failed to close/delete lock file: " + locktest); } } } log.debug("Chose global tmpdir: " + locktest.getParent()); break; // Something found! } if (locktest == null) { throw new RuntimeException("Could not find lockable tmp dir"); } File omero = new File(locktest.getParentFile(), "omero"); File tmp = new File(omero, "tmp"); return tmp; } /** * Returns the current OS-user's name. */ protected String username() { return System.getProperty("user.name"); } /** * Returns some representation of the current process's id */ protected String pid() { String pid = java.lang.management.ManagementFactory.getRuntimeMXBean() .getName(); return pid; } /** * Returns true if the current user can write to the give directory. */ protected boolean access(File dir) { return dir.canWrite(); } /** * If the given directory doesn't exist, creates it and returns true. * Otherwise false. Note: Java doesn't allow setting the mode for the * directory but it is intended to be 0700. */ protected boolean create(File dir) { if (!dir.exists()) { // Can't set permissions dir.mkdirs(); return true; } return false; } /** * Returns the directory under which all temporary files and folders will be * created. */ public File getTempDir() { return dir; } /** * Uses {@link File#createTempFile(String, String, File)} to create * temporary files and folders under the top-level temporary directory. * For folders, first a temporary file is created, then deleted, * and finally a directory produced. */ public File createPath(String prefix, String suffix, boolean folder) throws IOException { File file = File.createTempFile(prefix, suffix, this.dir); if (folder) { file.delete(); file.mkdirs(); log.debug("Added folder " + file.getAbsolutePath()); } else { log.debug("Added file " + file.getAbsolutePath()); } return file; } /** * If the given file is under the top-level temporary directory * then it is deleted whether file or folder. * Otherwise a {@link RuntimeException} is thrown. */ public void removePath(File file) throws IOException { String f = file.getAbsolutePath(); String d = dir.getAbsolutePath(); if (!f.startsWith(d)) { throw new RuntimeException(d + " is not in " + f); } if (file.exists()) { if (file.isDirectory()) { FileUtils.deleteDirectory(file); log.debug("Removed folder " + f); } else { file.delete(); log.debug("Removed file " + f); } } } /** * Deletes the top-level temporary directory. */ protected void cleanTempDir() throws IOException { log.debug("Removing tree: " + dir.getAbsolutePath()); FileUtils.deleteDirectory(dir); // Checks if dir exists } /** * Attempts to delete all directories under self.userdir other than the one * owned by this process. If a directory is locked, it is skipped. */ protected void cleanUserDir() throws IOException { log.debug("Cleaning user dir: " + userDir.getAbsolutePath()); List<File> files = Arrays.asList(userDir.listFiles()); final String d = dir.getCanonicalPath(); for (File file : files) { String f = file.getCanonicalPath(); if (f.equals(d)) { log.debug("Skipping self: " + d); continue; } File lock = new File(file, ".lock"); RandomAccessFile raf = new RandomAccessFile(lock, "rw"); try { FileLock fl = raf.getChannel().tryLock(); if (fl == null) { System.out.println("Locked: " + f); continue; } } catch (Exception e) { System.out.println("Locked: " + f); continue; } finally { raf.close(); } FileUtils.deleteDirectory(file); System.out.println("Deleted: " + f); } } // // Static methods // /** * Emulates {@link File#createTempFile(String, String)} by calling * {@link #create_path(String, String)}. */ public static File createTempFile(String prefix, String suffix) throws IOException { return create_path(prefix, suffix); } /** * Calls {@link #createPath(String, String, boolean)} * with defaults of "omero", ".tmp", and false. */ public static File create_path() throws IOException { return manager.createPath("omero", ".tmp", false); } /** * Calls {@link #createPath(String, String, boolean)} * with defaults of ".tmp", and false. */ public static File create_path(String prefix) throws IOException { return manager.createPath(prefix, ".tmp", false); } /** * Calls {@link #createPath(String, String, boolean)} * with ".tmp", and false arguments. */ public static File create_path(String prefix, String suffix) throws IOException { return manager.createPath(prefix, suffix, false); } /** * Calls {@link #createPath(String, String, boolean)}. */ public static File create_path(String prefix, String suffix, boolean folder) throws IOException { return manager.createPath(prefix, suffix, folder); } /** * Calls {@link #removePath(File)}. */ public static void remove_path(File file) throws IOException { manager.removePath(file); } /** * Calls {@link #getTempDir()}. */ public static void gettempdir() { manager.getTempDir(); } /** * Command-line interface to the global {@link TempFileManager} instance. * Valid arguments: "--debug", "clean", "dir", and for * testing, "lock" */ public static void main(String[] _args) throws IOException { List<String> args = Arrays.asList(_args); if (args.size() > 0) { // Debug may already be activated. See static block above. if (args.contains("--debug") && ! System.getenv().containsKey("DEBUG")) { // TODO: Needs addressing in slf4j-friendly way //ConsoleAppender console = new ConsoleAppender(); //console.setName("System.err"); //console.setTarget(ConsoleAppender.SYSTEM_ERR); //console.setLayout(new SimpleLayout()); //console.activateOptions(); //org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger("omero"); //logger.addAppender(console); //logger.setLevel(Level.DEBUG); //logger.addAppender(console); } if (args.contains("clean")) { manager.cleanUserDir(); System.exit(0); } else if (args.contains("dir")) { System.out.println(manager.getTempDir().getAbsolutePath()); System.exit(0); } else if (args.contains("test")) { File test = new File("/tmp/test"); if (test.exists()) { test.delete(); System.out.println("Deleted test"); } File f = create_path(); System.out.println(f.getAbsolutePath()); f.deleteOnExit(); FileUtils.writeStringToFile(f, "test"); FileUtils.moveFile(f, test); System.exit(0); } else if (args.contains("lock")) { System.out.println("Locking " + manager.getTempDir().getAbsolutePath()); System.out.println("Waiting on user input..."); System.in.read(); System.exit(0); } } System.err.println("Usage: TempFileManager clean"); System.err.println(" or: TempFileManager dir"); System.exit(2); } }