/******************************************************************************* * Copyright (c) 2011 Arapiki Solutions Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * "Peter Smith <psmith@arapiki.com>" - initial API and * implementation and/or initial documentation *******************************************************************************/ package com.buildml.utils.os; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.buildml.utils.errors.FatalError; /** * Various static methods for accessing the operating system's features. * * @author "Peter Smith <psmith@arapiki.com>" */ public class SystemUtils { /*=====================================================================================* * FIELDS/TYPE *=====================================================================================*/ /* * These flags are used as a bit map to inform traverseFileSystem which type of * files/directories/symlinks we're interested in knowing about. */ /** Flag to indicate we're interested in seeing files. */ public static final int REPORT_FILES = 1; /** Flag to indicate we're interested in seeing directories. */ public static final int REPORT_DIRECTORIES = 2; /** Flag to indicate we're interested in seeing symlinks. */ public static final int REPORT_SYMLINKS = 4; /*=====================================================================================* * PUBLIC METHODS *=====================================================================================*/ /** * Static block - called when the class is first loaded. This will load any native * libraries that we need. */ static { /* * Use the BUILDML_HOME environment variable to determine where our dynamically loadable * libraries are stored. */ String buildMlHome = System.getenv("BUILDML_HOME"); if (buildMlHome == null) { buildMlHome = System.getProperty("BUILDML_HOME"); if (buildMlHome == null) { throw new FatalError("The BUILDML_HOME environment variable is not set."); } } /* * load our JNI library. This could be in /lib if we're running a CLI-based mode, * or /files/lib/ if we're running as an Eclipse plugin. */ try { System.load(buildMlHome + "/lib/libnativeLib.so"); } catch (UnsatisfiedLinkError ex1) { try { System.load(buildMlHome + "/files/lib/libnativeLib.so"); } catch (UnsatisfiedLinkError ex2) { throw new FatalError("Unable to load native methods: " + ex2.getMessage(), ex2); } } } /*-------------------------------------------------------------------------------------*/ /** * Test whether a local file is a symlink (as opposed to a regular file or directory). * * @param fileName The file's name. * @return True if the file is a symlink, else false. * @exception FileNotFoundException If the file doesn't exist. */ public static native boolean isSymlink(String fileName) throws FileNotFoundException; /*-------------------------------------------------------------------------------------*/ /** * Read the target of the specified symlink. * * @param fileName The name of the symlink. * @return The target of the symlink, or null if it's not a symlink, or if some other * error occurred. * @exception FileNotFoundException If the file doesn't exist. */ public static native String readSymlink(String fileName) throws FileNotFoundException; /*-------------------------------------------------------------------------------------*/ /** * Create a new symlink on the local machine's file system. * * @param fileName The name of the symlink that will be created. * @param targetFileName The destination of the symlink. * @return 0 if the symlink was created successfully, otherwise non-zero. */ public static native int createSymlink(String fileName, String targetFileName); /*-------------------------------------------------------------------------------------*/ /** * Set the Unix file permissions on the specified file/directory. * * @param fileName The name of the file/directory to have permissions set. * @param mode The Unix mode (in octal, such as 0755). * @return 0 on success, or -1 on error. */ public static native int chmod(String fileName, int mode); /*-------------------------------------------------------------------------------------*/ /** * Execute a shell command and capture the return code, standard output and standard error * strings. * * @param args The arguments of the shell command to be executed. * @param stdin The text to be passed to the command's standard input. * @param echoStream If not null, the process's stdout and stderr should be echoed to this stream. * @param saveToBuffer Set if the stdout and stderr should be saved in buffers. * @param workingDir If not null, the working directory the command should be executed in. * * @return The command's standard output, error and return code in the form of a ShellResult object. If * saveToBuffer is false, then the output/error fields will be empty. * * @throws IOException For some reason the shell command failed to execute. * @throws InterruptedException The command was interrupted before it completed. */ public static ShellResult executeShellCmd( String args[], String stdin, PrintStream echoStream, boolean saveToBuffer, File workingDir) throws IOException, InterruptedException { /* * Figure out what the BUILDML_HOME environment variable must be set to. If it's already * set in the environment, that's great, but if not we should add it. */ String buildMlHome = System.getenv("BUILDML_HOME"); String newEnvironment[] = null; if (buildMlHome == null) { buildMlHome = System.getProperty("BUILDML_HOME"); /* whoops - not in the environment, or in the properties */ if (buildMlHome == null) { throw new IOException( "Unable to locate cfs tool. BUILDML_HOME environment variable not set."); } /* BUILDML_HOME is a property, but not an environment variable, so we must add it */ Map<String, String> envMap = System.getenv(); ArrayList<String> envArray = new ArrayList<String>(envMap.size() + 1); for (String key : envMap.keySet()) { envArray.add(key + "=" + envMap.get(key)); } envArray.add("BUILDML_HOME=" + buildMlHome); newEnvironment = envArray.toArray(new String[envArray.size()]); } /* invoke the command as a sub process */ Runtime run = Runtime.getRuntime(); Process pr = run.exec(args, newEnvironment, workingDir); /* send the child process it's stdin, followed by an EOF */ PrintStream childStdin = new PrintStream(pr.getOutputStream()); childStdin.print(stdin); childStdin.close(); /* * Now, to separate out the stdout and stderr, we need to create two worker threads. Each * thread reads the child process's stream into a string buffer, then terminates when the * EOF is reached. */ StreamToStringBufferWorker stdOutWorker = new StreamToStringBufferWorker(pr.getInputStream(), saveToBuffer, echoStream); StreamToStringBufferWorker stdErrWorker = new StreamToStringBufferWorker(pr.getErrorStream(), saveToBuffer, echoStream); /* Start the threads and wait for them both to terminate */ Thread stdOutThread = new Thread(stdOutWorker); Thread stdErrThread = new Thread(stdErrWorker); stdOutThread.start(); stdErrThread.start(); stdOutThread.join(); stdErrThread.join(); /* Wait for the child process to terminate - it probably has by now, but we need to be careful */ pr.waitFor(); /* * Place the child process's stdout, stderr and return code in a ShellResult structure. Note * that getString() can throw an exception if for some reason the corresponding worker thread * encountered an exception. We'll simply throw it back to our parent. */ ShellResult res = new ShellResult(stdOutWorker.getString(), stdErrWorker.getString(), pr.exitValue()); return res; } /*-------------------------------------------------------------------------------------*/ /** * A variant of executeShellCmd that defaults to saving output/error to a buffer, but not echoing it * * @param args The arguments of the command to be executed. * @param stdin The text to be passed to the command's standard input. * * @return The command's standard output, error and return code in the form of a ShellResult object. * * @throws IOException For some reason the shell command failed to execute. * @throws InterruptedException The command was interrupted before it completed. */ public static ShellResult executeShellCmd(String args[], String stdin) throws IOException, InterruptedException { return executeShellCmd(args, stdin, null, true, null); } /*-------------------------------------------------------------------------------------*/ /** * Traverse a file system and invoke a callback method on each file system entry (file, * directory or symlink). * * @param rootPath The starting (top) path for the traversal. The traversal will start * at this point, and traverse downwards through the file system sub-directories. * @param pathsToReport The types of paths to report - a bitmap of REPORT_FILES, * REPORT_DIRECTORIES and REPORT_SYMLINKS. * @param callbackObj The callback object to be invoked as each file system entry * is encountered. */ public static void traverseFileSystem(String rootPath, int pathsToReport, FileSystemTraverseCallback callbackObj) { File rootFile = new File(rootPath); traverseFileSystemHelper(rootFile, null, null, pathsToReport, callbackObj); } /*-------------------------------------------------------------------------------------*/ /** * Traverse a file system and invoke a callback method on each file system entry (file, * directory or symlink). * * @param rootPath The starting (top) path for the traversal. The traversal will start * at this point, and traverse downwards through the file system sub-directories. * @param matchFilePattern A Regex pattern specifying the types of file name we're * interested in hearing about (use null to match everything). * @param ignoreDirPattern A Regex pattern specifying the types of directory names we're * interested in skipping over and not traversing (use null to not skip over anything). * @param pathsToReport The types of paths to report - a bitmap of REPORT_FILES, * REPORT_DIRECTORIES and REPORT_SYMLINKS. * @param callbackObj The callback object to be invoked as each file system entry * is encountered. */ public static void traverseFileSystem(String rootPath, String matchFilePattern, String ignoreDirPattern, int pathsToReport, FileSystemTraverseCallback callbackObj) { File rootFile = new File(rootPath); Pattern mfPattern = null; if (matchFilePattern != null) { mfPattern = Pattern.compile(matchFilePattern); } Pattern idPattern = null; if (ignoreDirPattern != null) { idPattern = Pattern.compile(ignoreDirPattern); } traverseFileSystemHelper(rootFile, mfPattern, idPattern, pathsToReport, callbackObj); } /*-------------------------------------------------------------------------------------*/ /** * A recursive helper function for traverseFileSystem(). * * @param thisPath The current path being traversed. * @param mfPattern The matching file pattern. * @param idPattern The ignore directory pattern. * @param pathsToReport A bitmap of which path types should be reported. * @param callbackObj The object to "call back" when a matching path is found. */ private static void traverseFileSystemHelper(File thisPath, Pattern mfPattern, Pattern idPattern, int pathsToReport, FileSystemTraverseCallback callbackObj) { /* if the file doesn't actually exist on the local file system, there's nothing to do */ if (!thisPath.exists()) { return; } String fileName = thisPath.getName(); /* if the file is actually a directory, recursively visit each of the entries */ if (thisPath.isDirectory()) { /* check whether the caller wanted to exclude this directory from the traversal */ Matcher m = null; if (idPattern != null) { m = idPattern.matcher(fileName); } if ((m == null) || (!m.matches())) { /* if the user requested it, report this directory to them */ if ((pathsToReport & REPORT_DIRECTORIES) != 0) { callbackObj.callback(thisPath); } /* traverse the children */ File children [] = thisPath.listFiles(); if (children == null) { return; } for (int i = 0; i < children.length; i++) { traverseFileSystemHelper(children[i], mfPattern, idPattern, pathsToReport, callbackObj); } } } /* else if it's a file, and the user is interested in hearing about it */ else if (thisPath.isFile()) { if ((pathsToReport & REPORT_FILES) != 0) { /* if a pattern was provided, only report names that match */ Matcher m = null; if (mfPattern != null) { m = mfPattern.matcher(fileName); } if ((m == null) || (m.matches())){ callbackObj.callback(thisPath); } } } /* else, it's not a file or directory - throw an error, for now */ else { throw new Error("Found a path that isn't a file or directory: " + thisPath.toString()); } } /*-------------------------------------------------------------------------------------*/ /** * Create a temporary directory on the file system. * @return The newly-created directory. * @throws IOException If the directory couldn't be created. */ public static File createTempDir() throws IOException { File tmpDir = File.createTempFile("tempDir", null); if (!tmpDir.delete() || !tmpDir.mkdir()) { throw new IOException("Couldn't make temporary directory: " + tmpDir); } return tmpDir; } /*-------------------------------------------------------------------------------------*/ /** * Delete a file system file or directory (and all the files and sub-directories it may * contain). * @param fileOrDir The file or directory to be deleted. * @return True or false, to indicate whether the deletion was successful. */ public static boolean deleteDirectory(File fileOrDir) { if (fileOrDir.isDirectory()) { String[] children = fileOrDir.list(); for (int i=0; i < children.length; i++) { if (!deleteDirectory(new File(fileOrDir, children[i]))){ return false; } } } return fileOrDir.delete(); } /*-------------------------------------------------------------------------------------*/ }