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

org.apache.brooklyn.util.os.Os Maven / Gradle / Ivy

Go to download

Utility classes and methods developed for Brooklyn but not dependendent on Brooklyn or much else

There is a newer version: 1.1.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.brooklyn.util.os;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.JavaClassNames;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.osgi.OsgiUtil;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.Strings;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.Beta;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;

public class Os {

    private static final Logger log = LoggerFactory.getLogger(Os.class);
    
    private static final int TEMP_DIR_ATTEMPTS = 1000;

    private static final char SEPARATOR_UNIX = '/';
    private static final char SEPARATOR_WIN = '\\';
    
    public static final String LINE_SEPARATOR = System.getProperty("line.separator");

    private static final boolean isMSWin = testForMicrosoftWindows();

    /** returns the best tmp dir to use; see {@link TmpDirFinder} for the logic
     * (and the explanation why this is needed!) */
    public static String tmp() {
        Maybe tmp = tmpdir.get();
        if (tmp.isPresent()) return tmp.get();

        tmpdir.useWithWarning(System.getProperty("java.io.tmpdir"));
        return tmp.get();
    }
    
    private static TmpDirFinder tmpdir = new TmpDirFinder();
    
    /** utility for finding a usable (writable) tmp dir, preferring java.io.tmpdir
     * (unless it's weird, e.g. /private/tmp/xxx or /var/tmp/... as under OS X, and /tmp is valid),
     * falling back to ~/.tmp/ (and creating that) if the others are not usable
     * 

* it is weird if /tmp is not writable, but it does happen, hence this check *

* note you can also set java system property {@value #BROOKLYN_OS_TMPDIR_PROPERTY} * to force the use of a specific tmp space */ public static class TmpDirFinder { /** can be set as a jvm system property to force a particular tmp dir; directory must exist with the right permissions */ public static String BROOKLYN_OS_TMPDIR_PROPERTY = "brooklyn.os.tmpdir"; private String tmpdir = null; private boolean isFallback = false; public Maybe get() { if (isFallback()) log.debug("TmpDirFinder: using fallback tmp directory "+tmpdir, new Throwable("Caller using fallback tmp dir")); if (isFound()) return Maybe.of(tmpdir); if (find()) return Maybe.of(tmpdir); return Maybe.absent(newFailure("TmpDirFinder: No valid tmp dir can be found")); } public boolean isFallback() { return isFallback; } public boolean useWithWarning(String dir) { if (tmpdir==null) { tmpdir = dir; isFallback = true; log.warn("Unable to find a valid tmp dir; will use "+dir+" but with caution! See (debug) messages marked TmpDirFinder for more information."); return true; } return false; } public boolean isFound() { return tmpdir!=null; } protected synchronized boolean find() { if (isFound()) return true; String customtmp = System.getProperty(BROOKLYN_OS_TMPDIR_PROPERTY); if (customtmp!=null) { if (checkAndSet(customtmp)) return true; log.warn("TmpDirFinder: Custom tmp directory '"+customtmp+"' in "+BROOKLYN_OS_TMPDIR_PROPERTY+" is not a valid tmp dir; ignoring"); } String systmp = System.getProperty("java.io.tmpdir"); boolean systmpWeird = (systmp.contains("/var/") || systmp.startsWith("/private")); if (!systmpWeird) if (checkAndSet(systmp)) return true; if (checkAndSet(File.separator+"tmp")) return true; if (systmpWeird) if (checkAndSet(systmp)) return true; try { String hometmp = mergePaths(home(), ".tmp"); File hometmpF = new File(hometmp); hometmpF.mkdirs(); if (checkAndSet(hometmp)) return true; } catch (Exception e) { log.debug("TmpDirFinder: Cannot create tmp dir in user's home dir: "+e); } return false; } protected boolean checkAndSet(String candidate) { if (!check(candidate)) return false; // seems okay tmpdir = candidate; log.debug("TmpDirFinder: Selected tmp dir '"+candidate+"' as the best tmp working space"); return true; } protected boolean check(String candidate) { try { File f = new File(candidate); if (!f.exists()) { log.debug("TmpDirFinder: Candidate tmp dir '"+candidate+"' does not exist"); return false; } if (!f.isDirectory()) { log.debug("TmpDirFinder: Candidate tmp dir '"+candidate+"' is not a directory"); return false; } File f2 = new File(f, "brooklyn-tmp-check-"+Strings.makeRandomId(4)); if (!f2.createNewFile()) { log.debug("TmpDirFinder: Candidate tmp dir '"+candidate+"' cannot have files created inside it ("+f2+")"); return false; } if (!f2.delete()) { log.debug("TmpDirFinder: Candidate tmp dir '"+candidate+"' cannot have files deleted inside it ("+f2+")"); return false; } return true; } catch (Exception e) { log.debug("TmpDirFinder: Candidate tmp dir '"+candidate+"' is not valid: "+e); return false; } } protected IllegalStateException newFailure(String message) { return new IllegalStateException(message); } } /** user name */ public static String user() { return System.getProperty("user.name"); } /** user's home directory */ public static String home() { return System.getProperty("user.home"); } /** merges paths using forward slash (unix way); * now identical to {@link Os#mergePaths(String...)} but kept for contexts * where caller wants to indicate the target system should definitely be unix */ public static String mergePathsUnix(String ...items) { return Urls.mergePaths(items); } /** merges paths using forward slash as the "local OS file separator", because it is recognised on windows, * making paths more consistent and avoiding problems with backslashes being escaped. * empty segments are omitted. */ public static String mergePaths(String ...items) { char separatorChar = '/'; StringBuilder result = new StringBuilder(); for (String item: items) { if (Strings.isEmpty(item)) continue; if (result.length() > 0 && !isSeparator(result.codePointAt(result.length()-1))) result.append(separatorChar); result.append(item); } return result.toString(); } /** tries to delete a directory recursively; * use with care - see http://stackoverflow.com/questions/8320376/why-is-files-deletedirectorycontents-deprecated-in-guava. *

* also note this implementation refuses to delete / or ~ or anything else not passing {@link #checkSafe(File)}. * if you might really want to delete something like that, use {@link #deleteRecursively(File, boolean)}. */ @Beta public static DeletionResult deleteRecursively(File dir) { return deleteRecursively(dir, false); } /** * as {@link #deleteRecursively(File)} but includes safety checks to prevent deletion of / or ~ * or anything else not passing {@link #checkSafe(File)}, unless the skipSafetyChecks parameter is set */ @Beta public static DeletionResult deleteRecursively(File dir, boolean skipSafetyChecks) { if (dir==null) return new DeletionResult(null, true, null); try { if (!skipSafetyChecks) checkSafe(dir); FileUtils.deleteDirectory(dir); return new DeletionResult(dir, true, null); } catch (IllegalArgumentException e) { // See exception reported in https://issues.apache.org/jira/browse/BROOKLYN-72 // If another thread is changing the contents of the directory at the same time as // we delete it, then can get this exception. return new DeletionResult(dir, false, e); } catch (IOException e) { return new DeletionResult(dir, false, e); } } /** fails if the dir is not "safe" for deletion, currently length <= 2 or the home directory */ protected static void checkSafe(File dir) throws IOException { String dp = dir.getAbsolutePath(); dp = Strings.removeFromEnd(dp, "/"); if (dp.length()<=2) throw new IOException("Refusing instruction to delete "+dir+": name too short"); if (Os.home().equals(dp)) throw new IOException("Refusing instruction to delete "+dir+": it's the home directory"); } /** * @see {@link #deleteRecursively(File)} */ @Beta public static DeletionResult deleteRecursively(String dir) { if (dir==null) return new DeletionResult(null, true, null); return deleteRecursively(new File(dir)); } public static class DeletionResult { private final File file; private final boolean successful; private final Throwable throwable; public DeletionResult(File file, boolean successful, Throwable throwable) { this.file = file; this.successful = successful; this.throwable = throwable; } public boolean wasSuccessful() { return successful; } public DeletionResult throwIfFailed() { if (!successful) throw Exceptions.propagate(new IOException("Unable to delete '"+file+"': delete returned false", throwable)); return this; } public File getFile() { return file; } public Throwable getThrowable() { return throwable; } public T asNullIgnoringError() { return null; } public T asNullOrThrowing() { throwIfFailed(); return null; } } private static class FileDeletionHook { public FileDeletionHook(File f, boolean recursively) { this.path = f; this.recursively = recursively; } final File path; final boolean recursively; public void run() throws IOException { if (path.exists()) { if (recursively && path.isDirectory()) { Os.deleteRecursively(path); } else { path.delete(); } } } } private static final Map deletions = new LinkedHashMap(); private static void addShutdownFileDeletionHook(String path, FileDeletionHook hook) { // ensure Ordering class is loaded (else shutdown hook will fail as it can't load that class when shutting down) Ordering.natural(); synchronized (deletions) { if (deletions.isEmpty()) { Thread shutdownHook = new Thread() { @Override public void run() { log.debug("Shutting down, deleting: "+deletions); try { synchronized (deletions) { List pathsToDelete = new ArrayList(deletions.keySet()); Collections.sort(pathsToDelete, Strings.lengthComparator().reverse()); for (String path: pathsToDelete) { try { deletions.remove(path).run(); } catch (Exception e) { log.warn("Unable to delete '"+path+"' on shutdown: "+e); log.debug("Trace for shutdown deletion problem: "+e, e); } } } } catch (Exception e) { log.warn("Unable to delete one or more paths ("+deletions+") on shutdown: "+e); log.debug("Trace for shutdown deletion problem: "+e, e); } } }; if (OsgiUtil.isBrooklynInsideFramework()) OsgiUtil.addShutdownHook(shutdownHook); //bundle deactivator will call OsgiUtils.shutdown() to run hooks else Runtime.getRuntime().addShutdownHook(shutdownHook); //jvm exit will run hooks } FileDeletionHook oldHook = deletions.put(path, hook); if (oldHook!=null && oldHook.recursively) // prefer any hook which is recursive deletions.put(path, oldHook); } } /** deletes the given file or empty directory on exit *

* similar to {@link File#deleteOnExit()} except it is smart about trying to delete longer filenames first * (and the shutdown hook order does not use proprietary java hooks) *

* note this does not delete non-empty directories; see {@link #deleteOnExitRecursively(File)} for that */ public static void deleteOnExit(File directoryToDeleteIfEmptyOrFile) { addShutdownFileDeletionHook(directoryToDeleteIfEmptyOrFile.getAbsolutePath(), new FileDeletionHook(directoryToDeleteIfEmptyOrFile, false)); } /** deletes the given file or directory and, in the case of directories, any contents; * similar to apache commons FileUtils.cleanDirectoryOnExit but corrects a bug in that implementation * which causes it to fail if content is added to that directory after the hook is registered */ public static void deleteOnExitRecursively(File directoryToCleanOrFile) { addShutdownFileDeletionHook(directoryToCleanOrFile.getAbsolutePath(), new FileDeletionHook(directoryToCleanOrFile, true)); } /** causes empty directories from subsubdir up to and including dir to be deleted on exit; * useful e.g. if we create /tmp/brooklyn-test/foo/test1/ and someone else might create * /tmp/brooklyn-test/foo/test2/ and we'd like the last tear-down to result in /tmp/brooklyn-test being deleted! *

* returns number of directories queued for deletion so caller can check for errors if desired; * if dir is not an ancestor of subsubdir this logs a warning but does not throw */ public static int deleteOnExitEmptyParentsUpTo(File subsubDirOrFile, File dir) { if (subsubDirOrFile==null || dir==null) return 0; List dirsToDelete = new ArrayList(); File d = subsubDirOrFile; do { dirsToDelete.add(d); if (d.equals(dir)) break; d = d.getParentFile(); } while (d!=null); if (d==null) { log.warn("File "+subsubDirOrFile+" has no ancestor "+dir+": will not attempt to clean up with ancestors on exit"); // dir is not an ancestor if subsubdir return 0; } for (File f: dirsToDelete) deleteOnExit(f); return dirsToDelete.size(); } /** like {@link #deleteOnExitRecursively(File)} followed by {@link #deleteOnExitEmptyParentsUpTo(File, File)} */ public static void deleteOnExitRecursivelyAndEmptyParentsUpTo(File directoryToCleanOrFile, File highestAncestorToDelete) { deleteOnExitRecursively(directoryToCleanOrFile); deleteOnExitEmptyParentsUpTo(directoryToCleanOrFile, highestAncestorToDelete); } /** as {@link File#mkdirs()} but throwing on failure and returning the directory made for fluent convenience */ public static File mkdirs(File dir) { dir.mkdirs(); if (dir.isDirectory()) { return dir; } throw Exceptions.propagate(new IOException("Failed to create directory " + dir + (dir.isFile() ? "(is file)" : ""))); } /** writes given contents to a temporary file which will be deleted on exit */ public static File writeToTempFile(InputStream is, String prefix, String suffix) { return writeToTempFile(is, new File(Os.tmp()), prefix, suffix); } /** writes given contents to a temporary file which will be deleted on exit, located under the given directory */ public static File writeToTempFile(InputStream is, File tempDir, String prefix, String suffix) { Preconditions.checkNotNull(is, "Input stream required to create temp file for %s*%s", prefix, suffix); mkdirs(tempDir); File tempFile = newTempFile(prefix, suffix); OutputStream out = null; try { out = new FileOutputStream(tempFile); ByteStreams.copy(is, out); } catch (IOException e) { throw Throwables.propagate(e); } finally { Streams.closeQuietly(is); Streams.closeQuietly(out); } return tempFile; } public static File writePropertiesToTempFile(Properties props, String prefix, String suffix) { return writePropertiesToTempFile(props, new File(Os.tmp()), prefix, suffix); } public static File writePropertiesToTempFile(Properties props, File tempDir, String prefix, String suffix) { Preconditions.checkNotNull(props, "Properties required to create temp file for %s*%s", prefix, suffix); File tempFile; try { tempFile = File.createTempFile(prefix, suffix, tempDir); } catch (IOException e) { throw Throwables.propagate(e); } tempFile.deleteOnExit(); OutputStream out = null; try { out = new FileOutputStream(tempFile); props.store(out, "Auto-generated by Brooklyn"); } catch (IOException e) { throw Throwables.propagate(e); } finally { Streams.closeQuietly(out); } return tempFile; } /** * Tidy up a file path. *

* Removes duplicate or trailing path separators (Unix style forward * slashes only), replaces initial {@literal ~} with the * value of {@link #home()} and folds out use of {@literal ..} and * {@literal .} path segments. * * @see com.google.common.io.Files#simplifyPath(String) */ public static String tidyPath(String path) { Preconditions.checkNotNull(path, "path"); Iterable segments = Splitter.on("/").split(Files.simplifyPath(path)); if (Iterables.get(segments, 0).equals("~")) { // Always at least one segment after simplifyPath segments = Iterables.concat(ImmutableSet.of(Os.home()), Iterables.skip(segments, 1)); } String result = Joiner.on("/").join(segments); if (log.isTraceEnabled() && !result.equals(path)) log.trace("Quietly changing '{}' to '{}'", path, result); return result; } /** * Checks whether a file system path is absolute in a platform neutral way. *

* As a consequence of the platform neutrality some edge cases are * not handled correctly: *

    *
  • On Windows relative paths starting with slash (either forward or backward) or ~ are treated as absolute. *
  • On UNIX relative paths starting with X:/ are treated as absolute. *
* * @param path A string representing a file system path. * @return whether the path is absolute under the above constraints. */ public static boolean isAbsolutish(String path) { return Strings.isNonBlank(path) && (path.codePointAt(0) == SEPARATOR_UNIX || path.equals("~") || path.startsWith("~" + SEPARATOR_UNIX) || (path.length() >= 3 && path.codePointAt(1) == ':' && isSeparator(path.codePointAt(2)))); } private static boolean isSeparator(int sep) { return sep == SEPARATOR_UNIX || sep == SEPARATOR_WIN; } public static String fromHome(String path) { return new File(Os.home(), path).getAbsolutePath(); } public static String nativePath(String path) { return new File(path).getPath(); } public static boolean isMicrosoftWindows() { return isMSWin; } private static boolean testForMicrosoftWindows() { String os = System.getProperty("os.name").toLowerCase(); //see org.apache.commons.lang.SystemUtils.IS_WINDOWS return os.startsWith("windows"); } /** creates a private temp file which will be deleted on exit; * either prefix or ext may be null; * if ext is non-empty and not > 4 chars and not starting with a ., then a dot will be inserted; * if either name part is too long it will be shortened to prevent filesystem errors */ public static File newTempFile(String prefix, String ext) { String sanitizedPrefix = (Strings.isNonEmpty(prefix) ? Strings.makeValidFilename(prefix) + "-" : ""); if (sanitizedPrefix.length()>101) sanitizedPrefix = sanitizedPrefix.substring(0, 100)+"--"; String extWithPrecedingSeparator = (Strings.isNonEmpty(ext) ? ext.startsWith(".") || ext.length()>4 ? ext : "."+ext : ""); if (extWithPrecedingSeparator.length()>13) sanitizedPrefix = sanitizedPrefix.substring(0, 12)+"--"; try { File tempFile = File.createTempFile(sanitizedPrefix, extWithPrecedingSeparator, new File(tmp())); tempFile.deleteOnExit(); return tempFile; } catch (IOException e) { throw Exceptions.propagate(e); } } /** as {@link #newTempFile(String, String)} using the class as the basis for a prefix */ public static File newTempFile(Class clazz, String ext) { return newTempFile(JavaClassNames.cleanSimpleClassName(clazz), ext); } /** creates a temp dir which will be deleted on exit */ public static File newTempDir(String prefix) { String sanitizedPrefix = (prefix==null ? "" : Strings.makeValidFilename(prefix) + "-"); String tmpParent = tmp(); //With lots of stale temp dirs it is possible to have //name collisions so we need to retry until a unique //name is found for (int i = 0; i < TEMP_DIR_ATTEMPTS; i++) { String baseName = sanitizedPrefix + Identifiers.makeRandomId(4); File tempDir = new File(tmpParent, baseName); if (!tempDir.exists()) { if (tempDir.mkdir()) { Os.deleteOnExitRecursively(tempDir); return tempDir; } else { log.warn("Attempt to create temp dir failed " + tempDir + ". Either an IO error (disk full, no rights) or someone else created the folder after the !exists() check."); } } else { log.debug("Attempt to create temp dir failed, already exists " + tempDir + ". With ID of length 4 it is not unusual (15% chance) to have duplicate names at the 2000 samples mark."); } } throw new IllegalStateException("cannot create temporary folders in parent " + tmpParent + " after " + TEMP_DIR_ATTEMPTS + " attempts."); } /** as {@link #newTempDir(String)}, using the class as the basis for a prefix */ public static File newTempDir(Class clazz) { return newTempDir(JavaClassNames.cleanSimpleClassName(clazz)); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy