/* * Copyright (C) 2012 RoboVM AB * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>. */ package org.robovm.compiler.util.io; import java.io.File; import java.io.IOException; import java.nio.file.FileStore; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import org.apache.commons.io.FileUtils; import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; import org.robovm.compiler.config.OS; import org.robovm.compiler.log.Logger; import org.robovm.compiler.util.Executor; import com.dd.plist.NSDictionary; import com.dd.plist.NSNumber; import com.dd.plist.NSObject; import com.dd.plist.PropertyListParser; /** * Will modify cache and tmpdir paths given a {@link Config#builder()} and prun * the cache if necessary. * * @author badlogic * */ public class RamDiskTools { private static final String ROBOVM_RAM_DISK_PATH = "/Volumes/RoboVM RAM Disk"; private static final long MIN_FREE_SPACE = 1024 * 1024 * 400; private File newCacheDir; private File newTmpDir; public File getCacheDir() { return newCacheDir; } public File getTmpDir() { return newTmpDir; } /** * Checks if a RAM disk is available and prunes it if necessary. */ public void setupRamDisk(Config config, File cacheDir, File tmpDir) { this.newCacheDir = cacheDir; this.newTmpDir = tmpDir; if (OS.getDefaultOS() != OS.macosx) { return; } File volume = new File(ROBOVM_RAM_DISK_PATH); if (!volume.exists()) { try { FileStore store = Files.getFileStore(new File(System.getProperty("user.home")) .toPath()); String plist = new Executor(Logger.NULL_LOGGER, "diskutil").args("info", "-plist", store.name()) .execCapture(); NSDictionary dict = (NSDictionary)PropertyListParser.parse(plist.getBytes("UTF-8")); NSObject value = dict.objectForKey("SolidState"); if(value == null || (value instanceof NSNumber && !((NSNumber)value).boolValue())) { // @formatter:off config.getLogger().warn("RoboVM has detected that you are running on a slow HDD. Please consider mounting a RAM disk.\n" + "To create a 2GB RAM disk, run this in your terminal:\n" + "SIZE=2048 ; diskutil erasevolume HFS+ 'RoboVM RAM Disk' `hdiutil attach -nomount ram://$((SIZE * 2048))`\n" + "See http://docs.robovm.com/ for more info"); // @formatter:on } } catch (Throwable t) { // nothing to do here, can't decide if we are on a SSD or HDD t.printStackTrace(); } return; } try { FileStore store = Files.getFileStore(volume.toPath()); if (store.getUsableSpace() < MIN_FREE_SPACE) { cleanRamDisk(store, volume, config); if (store.getUsableSpace() < MIN_FREE_SPACE) { config.getLogger().info("Couldn't free enough space on RAM disk, using hard drive"); return; } } File newCacheDir = new File(volume, "cache"); if (!newCacheDir.exists() && !newCacheDir.mkdirs()) { config.getLogger().info("Couldn't create cache directory on RAM disk, using hard drive"); return; } File newTmpDir = new File(volume, "tmp"); if (!newTmpDir.exists() && !newTmpDir.mkdirs()) { config.getLogger().info("Couldn't create tmp directory on RAM disk, using hard drive"); return; } newTmpDir = new File(newTmpDir, tmpDir.getAbsolutePath()); config.getLogger().info("Using RAM disk at %s for cache and tmp directory", ROBOVM_RAM_DISK_PATH); this.newCacheDir = newCacheDir; this.newTmpDir = newTmpDir; } catch (Throwable t) { config.getLogger().error("Couldn't setup RAM disk, using hard drive, %s", t.getMessage()); this.newCacheDir = cacheDir; this.newTmpDir = tmpDir; } } private void cleanRamDisk(FileStore store, File volume, Config config) { // clean the cache/ and tmp/ dirs // FIXME be smarter as per the issue report try { // FileUtils.deleteDirectory(new File(volume, "tmp")); // only clean the cache if killing the tmp dir didn't work if (store.getUsableSpace() < MIN_FREE_SPACE) { cleanCache(store, volume, config); } } catch (IOException e) { // nothing to do here } } private void cleanCache(FileStore store, File volume, Config config) throws IOException { OS currOs = config.getOs(); Arch currArch = config.getArch(); CacheDir currCacheDir = constructCacheDir(volume, currOs, currArch, config.isDebug()); // Enumerate all directories that are not our current cache // dir List<CacheDir> cacheDirs = new ArrayList<CacheDir>(); for (OS os : OS.values()) { for (Arch arch : Arch.values()) { for (boolean isDebug : new boolean[] { false, true }) { CacheDir cacheDir = constructCacheDir(volume, os, arch, isDebug); if (cacheDir != null && !cacheDir.directory.equals(currCacheDir.directory)) { cacheDirs.add(cacheDir); } } } } // sort the directories by their last modified // date in ascending order (oldest first). We // start deleting the oldest cache files first // before we start deleting the cache files for // the current os/arch Collections.sort(cacheDirs, new Comparator<CacheDir>() { @Override public int compare(CacheDir o1, CacheDir o2) { return new Date(o1.lastModified).compareTo(new Date(o2.lastModified)); } }); // add our current target dir last if it already // exists. This way we delete its cache files last if(currCacheDir != null) { cacheDirs.add(currCacheDir); } // start deleting files until we have enough // space for(CacheDir dir: cacheDirs) { for(File file: dir.objFiles) { file.delete(); if(store.getUsableSpace() > MIN_FREE_SPACE) { return; } } } // nuclear option, we couldn't delete enough files FileUtils.deleteDirectory(new File(volume, "cache")); } private CacheDir constructCacheDir(File volume, OS os, Arch arch, boolean isDebug) { File dir = new File(volume, "cache/" + os.toString() + "/" + arch.toString() + "/" + (isDebug ? "debug" : "release")); if (!dir.exists()) return null; List<File> objFiles = new ArrayList<File>((Collection<File>) FileUtils.listFiles(dir, new String[] { "o" }, true)); Collections.sort(objFiles, new Comparator<File>() { @Override public int compare(File f1, File f2) { return new Date(f2.lastModified()).compareTo(new Date(f1.lastModified())); } }); long lastModified = 0; for (File file : objFiles) { lastModified = Math.max(lastModified, file.lastModified()); } return new CacheDir(dir, objFiles, lastModified); } static class CacheDir { File directory; List<File> objFiles; long lastModified; public CacheDir(File directory, List<File> objFiles, long lastModified) { this.directory = directory; this.objFiles = objFiles; this.lastModified = lastModified; } } }