
org.apache.brooklyn.util.os.Os Maven / Gradle / Ivy
Show all versions of brooklyn-utils-common Show documentation
/*
* 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.os.Os;
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.io.ByteStreams;
import com.google.common.io.Files;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.brooklyn.util.osgi.OsgiUtil;
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) {
synchronized (deletions) {
if (deletions.isEmpty()) {
Thread shutdownHook = new Thread() {
@Override
public void run() {
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);
}
}
}
}
};
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
path.codePointAt(0) == SEPARATOR_UNIX ||
path.equals("~") || path.startsWith("~" + SEPARATOR_UNIX) ||
path.length()>=3 && path.codePointAt(1) == ':' &&
isSeparator(path.codePointAt(2));
}
/** @deprecated since 0.7.0, use {@link #isAbsolutish(String)} */
@Deprecated
public static boolean isAbsolute(String path) {
return isAbsolutish(path);
}
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 */
public static File newTempFile(String prefix, String ext) {
String sanitizedPrefix = (Strings.isNonEmpty(prefix) ? Strings.makeValidFilename(prefix) + "-" : "");
String extWithPrecedingSeparator = (Strings.isNonEmpty(ext) ? ext.startsWith(".") || ext.length()>4 ? ext : "."+ext : "");
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 ? "" : 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));
}
}