/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.node.updater; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.DataInputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.MalformedURLException; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Properties; import java.util.Set; import java.util.TreeSet; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.tanukisoftware.wrapper.WrapperManager; import freenet.client.FetchException; import freenet.crypt.SHA256; import freenet.keys.FreenetURI; import freenet.node.PrioRunnable; import freenet.node.Version; import freenet.support.Executor; import freenet.support.Fields; import freenet.support.HexUtil; import freenet.support.Logger; import freenet.support.io.Closer; import freenet.support.io.FileBucket; import freenet.support.io.FileUtil; import freenet.support.io.FileUtil.CPUArchitecture; import freenet.support.io.FileUtil.OperatingSystem; import freenet.support.io.NativeThread; /** * Parses the dependencies.properties file and ensures we have all the * libraries required to use the next version. Calls the Deployer to do the * actual fetches, and to deploy the new version when we have everything * ready. * * We used to support a range of freenet-ext.jar versions. However, * supporting ranges creates a lot of complexity, especially with Update * Over Mandatory support. * * File format of dependencies.properties: * [module].type=[module type] * CLASSPATH means the file must be downloaded, and then added to the * classpath in wrapper.conf, before the update can be loaded. * * OPTIONAL_PRELOAD means we just want to download the file. * * [module].version=[version number] * Can often be parsed from MANIFEST.MF in Jar's, but that is NOT mandatory. * * [module].filename=[preferred filename] * For CLASSPATH, this should be unique, i.e. include the version in the * filename, e.g. freenet-ext-29.jar. For OPTIONAL_PRELOAD, we will often * overwrite existing files. * * [module].sha256=[hash in hex] * SHA256 hash of the file. * * [module].filename-regex=[regular expression] * Matches filenames for this module. Only required for CLASSPATH. Note that * filenames will be toLowerCase()'ed first (but the regex isn't). * * [module].key=[CHK URI] * Where to fetch the file from if we don't have it. * * [module].size=[decimal size in bytes] * Size of the file. * * Optional: * * [module].order=[decimal integer order, default is 0] * Ordering of CLASSPATH files within the wrapper.conf. E.g. freenet-ext.jar * is usually the last element because we want the earlier files to override * classes in it. * * [module].os=[comma delimited list of OS's and pseudo-OS's] * OS's: See FileUtil.OperatingSystem: MacOS Linux FreeBSD GenericUnix Windows * Pseudo-Os's: ALL_WINDOWS ALL_UNIX ALL_MAC (these correspond to the booleans * on FileUtil.OperatingSystem). * * @author toad * */ public class MainJarDependenciesChecker { private static volatile boolean logMINOR; static { Logger.registerClass(MainJarDependenciesChecker.class); } // Lightweight interfaces, mundane glue code implemented by the caller. // FIXME unit testing should be straightforward, AND WOULD BE A GOOD IDEA! class MainJarDependencies { /** The freenet.jar build to be deployed. It might be possible to * deploy a new build without changing the wrapper. */ final int build; /** The actual dependencies. */ final Set<Dependency> dependencies; /** True if we must rewrite wrapper.conf, i.e. if any new jars have * been added, or new versions of existing jars. Won't be reliably * true in case of jars being removed at present. FIXME see comments * in handle() about deletion placeholders! */ final boolean mustRewriteWrapperConf; MainJarDependencies(TreeSet<Dependency> dependencies, int build) { this.dependencies = Collections.unmodifiableSortedSet(dependencies); this.build = build; boolean mustRewrite = false; for(Dependency d : dependencies) { if(d.oldFilename == null || !d.oldFilename.equals(d.newFilename)) { mustRewrite = true; break; } if(File.pathSeparatorChar == ':' && d.oldFilename != null && d.oldFilename.getName().equalsIgnoreCase("freenet-ext.jar.new")) { // If wrapper.conf currently contains freenet-ext.jar.new, we need to update wrapper.conf even // on unix. Reason: freenet-ext.jar.new won't be read if it's not the first item on the classpath, // because freenet.jar includes freenet-ext.jar implicitly via its manifest. mustRewrite = true; break; } } mustRewriteWrapperConf = mustRewrite; } } interface Deployer { public void deploy(MainJarDependencies deps); public JarFetcher fetch(FreenetURI uri, File downloadTo, long expectedLength, byte[] expectedHash, JarFetcherCallback cb, int build, boolean essential, boolean executable) throws FetchException; /** Called by cleanup with the dependencies we can serve for the current version. * @param expectedHash The hash of the file's contents, which is also * listed in the dependencies file. * @param filename The local file to serve it from. */ public void addDependency(byte[] expectedHash, File filename); /** We have just downloaded a dependency needed for the current build. Reannounce to tell * our peers about it. */ public void reannounce(); /** A multi-file update (e.g. wrapper update) is ready to deploy. It may need a restart. * We may need the user's permission to deploy it, or we may be able to deploy it * immediately. The Deployer must call atomicDeployer.deployMultiFileUpdateOffThread() * when ready. * @param atomicDeployer */ public void multiFileReplaceReadyToDeploy(AtomicDeployer atomicDeployer); } interface JarFetcher { public void cancel(); } interface JarFetcherCallback { public void onSuccess(); public void onFailure(FetchException e); } /** A dependency, for purposes of writing the new wrapper.conf. Contains its new filename, its * priority (order) in the wrapper.conf classpath, and all that is needed to identify the * previous line referring to this file. * @author toad */ final class Dependency implements Comparable<Dependency> { /** The old filename, if known. This will be in wrapper.conf. */ private File oldFilename; /** The new filename, to which we will download the file. */ private File newFilename; /** Pattern to recognise filenames for this dependency in the last resort. */ private Pattern regex; /** Priority of the dependency within the wrapper.conf classpath. Smaller value = earlier * in the classpath = used first. */ private int order; private Dependency(File oldFilename, File newFilename, Pattern regex, int order) { this.oldFilename = oldFilename; this.newFilename = newFilename; this.regex = regex; this.order = order; } public File oldFilename() { return oldFilename; } public File newFilename() { return newFilename; } public Pattern regex() { return regex; } @Override public int compareTo(Dependency arg0) { if(this == arg0) return 0; if(order > arg0.order) return 1; else if(order < arg0.order) return -1; // Filename comparisons aren't very reliable (e.g. "./test" versus "test" are not equals()!), go by getName() first. int ret = newFilename.getName().compareTo(arg0.newFilename.getName()); if(ret != 0) return ret; return newFilename.compareTo(arg0.newFilename); } public int order() { return order; } } MainJarDependenciesChecker(Deployer deployer, Executor executor) { this.deployer = deployer; this.executor = executor; } private final Deployer deployer; /** The final filenames we will use in the update, which we have * already downloaded. */ private final TreeSet<Dependency> dependencies = new TreeSet<Dependency>(); /** Set if the update can't be deployed because the dependencies file is * broken. We should wait for an update with a valid file. */ private boolean broken = false; /** The build we are about to deploy */ private int build; private class Downloader implements JarFetcherCallback { /** The JarFetcher which fetches the dependency from Freenet or via UOM. */ final JarFetcher fetcher; /** The dependency. Will be added to the set of downloaded dependencies after the fetch * completes if this is an essential dependency for the build currently being fetched. */ final Dependency dep; /** True if this dependency is required prior to deploying the next build. False if it's * just OPTIONAL_PRELOAD. */ final boolean essential; /** The build number that this dependency is being downloaded for */ final int forBuild; /** Construct with a Dependency, so we can add it when we're done. */ Downloader(Dependency dep, FreenetURI uri, byte[] expectedHash, long expectedSize, boolean essential, boolean executable, int forBuild) throws FetchException { fetcher = deployer.fetch(uri, dep.newFilename, expectedSize, expectedHash, this, build, essential, executable); this.dep = dep; this.essential = essential; this.forBuild = forBuild; } @Override public void onSuccess() { if(!essential) { System.out.println("Downloaded "+dep.newFilename+" - may be used by next update"); return; } System.out.println("Downloaded "+dep.newFilename+" needed for update "+forBuild+"..."); boolean toDeploy = false; boolean forCurrentVersion = false; synchronized(MainJarDependenciesChecker.this) { downloaders.remove(this); if(forBuild == build) { // If the dependency is for the build we are about to deploy... dependencies.add(dep); toDeploy = ready(); } else { forCurrentVersion = (forBuild == Version.buildNumber()); } } if(toDeploy) deploy(); else if(forCurrentVersion) deployer.reannounce(); } @Override public void onFailure(FetchException e) { if(!essential) { Logger.error(this, "Failed to pre-load "+dep.newFilename+" : "+e, e); } else { System.err.println("Failed to fetch "+dep.newFilename+" needed for next update ("+e.getShortMessage()+"). Will try again if we find a new freenet.jar."); synchronized(MainJarDependenciesChecker.this) { downloaders.remove(this); if(forBuild != build) return; broken = true; } } } public void cancel() { fetcher.cancel(); } } /** The dependency downloads currently running which are required for the next build. Hence * non-essential (preload) dependencies are not added to this set. */ private final HashSet<Downloader> downloaders = new HashSet<Downloader>(); private final Executor executor; /** Parse the Properties file. Check whether we have the jars it refers to. * If not, start fetching them. * @param props The Properties parsed from the dependencies.properties file. * @return The set of filenames needed if we can deploy immediately, in * which case the caller MUST deploy. */ public synchronized MainJarDependencies handle(Properties props, int build) { try { return innerHandle(props, build); } catch (RuntimeException e) { broken = true; Logger.error(this, "MainJarDependencies parsing update dependencies.properties file broke: "+e, e); throw e; } catch (Error e) { broken = true; Logger.error(this, "MainJarDependencies parsing update dependencies.properties file broke: "+e, e); throw e; } } enum DEPENDENCY_TYPE { /** A jar we want to put on the classpath. Normally we move to a new filename when there is * a new version of such a dependency; supports most features of dependencies.properties. */ CLASSPATH, /** A jar we want to put on the classpath but after that we won't update it even if there * is a new version. Used for wrapper.jar since we will update it via a separate mechanism, * because we have to update other files too. No regex support - must match the exact * filename. We do however check for 0 length files just in case. */ OPTIONAL_CLASSPATH_NO_UPDATE, /** A file to download, which does not block the update. */ OPTIONAL_PRELOAD, /** Deploy multiple files at once, all or nothing, then do a full restart on the wrapper. * On Windows this needs an external EXE which waits for shutdown, replaces the files, then * starts Freenet back up; on Linux and Mac we can just use a shell script. */ OPTIONAL_ATOMIC_MULTI_FILES_WITH_RESTART; final boolean optional; DEPENDENCY_TYPE() { this.optional = this.name().startsWith("OPTIONAL_"); } } private synchronized MainJarDependencies innerHandle(Properties props, int build) { // FIXME support deletion placeholders. // I.e. when we remove a library we put a placeholder in to tell this code to delete it. // It's not acceptable to just delete stuff we don't know about. clear(build); HashSet<String> processed = new HashSet<String>(); File[] list = new File(".").listFiles(new FileFilter() { @Override public boolean accept(File arg0) { if(!arg0.isFile()) return false; // Ignore non-jars regardless of what the regex says. String name = arg0.getName().toLowerCase(); if(!(name.endsWith(".jar") || name.endsWith(".jar.new"))) return false; // FIXME similar checks elsewhere, factor out? if(name.equals("freenet.jar") || name.equals("freenet.jar.new") || name.equals("freenet-stable-latest.jar") || name.equals("freenet-stable-latest.jar.new")) return false; return true; } }); outer: for(String propName : props.stringPropertyNames()) { if(!propName.contains(".")) continue; String baseName = propName.split("\\.")[0]; if(!processed.add(baseName)) continue; String s = props.getProperty(baseName+".type"); if(s == null) { Logger.error(this, "dependencies.properties broken? missing type for \""+baseName+"\""); broken = true; continue; } DEPENDENCY_TYPE type; try { type = DEPENDENCY_TYPE.valueOf(s); if(type == DEPENDENCY_TYPE.OPTIONAL_ATOMIC_MULTI_FILES_WITH_RESTART) { // Ignore. Handle in cleanup(). continue; } } catch (IllegalArgumentException e) { if(s.startsWith("OPTIONAL_")) { // We don't understand it, but that's OK as it's optional. if(logMINOR) Logger.minor(this, "Ignoring non-essential dependency type \""+s+"\" for \""+baseName+"\""); continue; } // We don't understand it, and it's not optional, so we can't deploy the update. Logger.error(this, "dependencies.properties broken? unrecognised type for \""+baseName+"\""); broken = true; continue; } // Check operating system restrictions. s = props.getProperty(baseName+".os"); if(s != null) { if(!matchesCurrentOS(s)) { Logger.normal(this, "Ignoring "+baseName+" as not relevant to this operating system"); continue; } } // Check architecture restrictions. s = props.getProperty(baseName+".arch"); if(s != null) { if(!matchesCurrentArch(s)) { Logger.normal(this, "Ignoring "+baseName+" as not relevant to this architecture"); continue; } } // Version is used in cleanup(). String version = props.getProperty(baseName+".version"); if(version == null) { Logger.error(this, "dependencies.properties broken? missing version"); broken = true; continue; } File filename = null; s = props.getProperty(baseName+".filename"); // FIXME use nodeDir if(s != null) filename = new File(s); if(filename == null) { Logger.error(this, "dependencies.properties broken? missing filename"); broken = true; continue; } if(filename.getParentFile() != null) filename.getParentFile().mkdirs(); FreenetURI maxCHK = null; s = props.getProperty(baseName+".key"); if(s == null) { Logger.error(this, "dependencies.properties broken? missing "+baseName+".key"); // Can't fetch it. :( } else { try { maxCHK = new FreenetURI(s); } catch (MalformedURLException e) { Logger.error(this, "Unable to parse CHK for "+baseName+": \""+s+"\": "+e, e); maxCHK = null; } } // FIXME where to get the proper folder from? That seems to be an issue in UpdateDeployContext as well... Pattern p = null; if(type == DEPENDENCY_TYPE.CLASSPATH) { // Regex used for matching filenames. String regex = props.getProperty(baseName+".filename-regex"); if(regex == null && type == DEPENDENCY_TYPE.CLASSPATH) { // Not a critical error. Just means we can't clean it up, and can't identify whether we already have a compatible jar. Logger.error(this, "No "+baseName+".filename-regex in dependencies.properties - we will not be able to clean up old versions of files, and may have to download the latest version unnecessarily"); // May be fatal later on depending on what else we have. } try { if(regex != null) p = Pattern.compile(regex); } catch (PatternSyntaxException e) { Logger.error(this, "Bogus Pattern \""+regex+"\" in dependencies.properties"); p = null; } } byte[] expectedHash = parseExpectedHash(props.getProperty(baseName+".sha256"), baseName); if(expectedHash == null) { System.err.println("Unable to update to build "+build+": dependencies.properties broken: No hash for "+baseName); broken = true; continue; } s = props.getProperty(baseName+".size"); long size = -1; if(s != null) { try { size = Long.parseLong(s); } catch (NumberFormatException e) { size = -1; } } if(size < 0) { System.err.println("Unable to update to build "+build+": dependencies.properties broken: Broken length for "+baseName+" : \""+s+"\""); broken = true; continue; } int order = 0; File currentFile = null; if(type == DEPENDENCY_TYPE.CLASSPATH || type == DEPENDENCY_TYPE.OPTIONAL_CLASSPATH_NO_UPDATE) { s = props.getProperty(baseName+".order"); if(s != null) { try { // Order is an optional field. // For most stuff we don't care. // But if it's present it must be correct! order = Integer.parseInt(s); } catch (NumberFormatException e) { System.err.println("Unable to update to build "+build+": dependencies.properties broken: Broken order for "+baseName+" : \""+s+"\""); broken = true; continue; } } currentFile = getDependencyInUse(p); } // Executable? boolean executable = false; s = props.getProperty(baseName+".executable"); if(s != null) { executable = Boolean.parseBoolean(s); } if(type == DEPENDENCY_TYPE.OPTIONAL_CLASSPATH_NO_UPDATE && filename.exists()) { if(filename.canRead() && filename.length() > 0) { System.out.println("Assuming non-updated dependency file is current: "+filename); dependencies.add(new Dependency(currentFile, filename, p, order)); continue; } else { System.out.println("Non-updated dependency is empty?: "+filename+" - will try to fetch it"); filename.delete(); } } if(validFile(filename, expectedHash, size, executable)) { // Nothing to do. Yay! System.out.println("Found file required by the new Freenet version: "+filename); // Use it. if(type == DEPENDENCY_TYPE.CLASSPATH) dependencies.add(new Dependency(currentFile, filename, p, order)); continue; } // Check the version currently in use. if(currentFile != null && validFile(currentFile, expectedHash, size, executable)) { System.out.println("Existing version of "+currentFile+" is OK for update."); // Use it. if(type == DEPENDENCY_TYPE.CLASSPATH) dependencies.add(new Dependency(currentFile, currentFile, p, order)); continue; } if(type == DEPENDENCY_TYPE.CLASSPATH) { if(p == null) { // No way to check existing files. if(maxCHK != null) { try { fetchDependency(maxCHK, new Dependency(currentFile, filename, p, order), expectedHash, size, true, executable); } catch (FetchException fe) { broken = true; Logger.error(this, "Failed to start fetch: "+fe, fe); System.err.println("Failed to start fetch of essential component for next release: "+fe); } } else { // Critical error. System.err.println("Unable to fetch "+baseName+" because no URI and no regex to match old versions."); broken = true; continue; } continue; } for(File f : list) { String name = f.getName(); if(!p.matcher(name.toLowerCase()).matches()) continue; if(validFile(f, expectedHash, size, executable)) { // Use it. System.out.println("Found "+name+" - meets requirement for "+baseName+" for next update."); dependencies.add(new Dependency(currentFile, f, p, order)); continue outer; } } } if(maxCHK == null) { System.err.println("Cannot fetch "+baseName+" for update because no CHK and no old file"); broken = true; continue; } // Otherwise we need to fetch it. try { fetchDependency(maxCHK, new Dependency(currentFile, filename, p, order), expectedHash, size, type != DEPENDENCY_TYPE.OPTIONAL_PRELOAD, executable); } catch (FetchException e) { broken = true; Logger.error(this, "Failed to start fetch: "+e, e); System.err.println("Failed to start fetch of essential component for next release: "+e); } } if(ready()) return new MainJarDependencies(new TreeSet<Dependency>(dependencies), build); else return null; } private static boolean matchesCurrentOS(String s) { OperatingSystem myOS = FileUtil.detectedOS; String[] osList = s.split(","); for(String os : osList) { os = os.trim(); if(myOS.toString().equalsIgnoreCase(os)) { return true; } if(os.equalsIgnoreCase("ALL_WINDOWS") && myOS.isWindows) { return true; } if(os.equalsIgnoreCase("ALL_UNIX") && myOS.isUnix) { return true; } if(os.equalsIgnoreCase("ALL_MAC") && myOS.isMac) { return true; } } return false; } private static boolean matchesCurrentArch(String s) { CPUArchitecture myCPU = FileUtil.detectedArch; String[] archList = s.split(","); for(String arch : archList) { arch = arch.trim(); if(myCPU.toString().equalsIgnoreCase(arch)) { return true; } } return false; } /** Should be called on startup, before any fetches have started. Will * remove unnecessary files and start blob fetches for files we don't * have blobs for. * @param props The dependencies.properties from the running version. * @return True unless something went wrong. */ public boolean cleanup(Properties props, final Deployer deployer, int build) { // This method should not change anything, but can call the callbacks. HashSet<String> processed = new HashSet<String>(); final ArrayList<File> toDelete = new ArrayList<File>(); File[] listMain = new File(".").listFiles(new FileFilter() { @Override public boolean accept(File arg0) { if(!arg0.isFile()) return false; String name = arg0.getName().toLowerCase(); // Cleanup old updater tempfiles. if(name.endsWith(NodeUpdateManager.TEMP_FILE_SUFFIX) || name.endsWith(NodeUpdateManager.TEMP_BLOB_SUFFIX)) { toDelete.add(arg0); return false; } // Ignore non-jars regardless of what the regex says. if(!name.endsWith(".jar")) return false; // FIXME similar checks elsewhere, factor out? if(name.equals("freenet.jar") || name.equals("freenet.jar.new") || name.equals("freenet-stable-latest.jar") || name.equals("freenet-stable-latest.jar.new")) return false; return true; } }); for(File f : toDelete) { System.out.println("Deleting old temp file \""+f+"\""); f.delete(); } for(String propName : props.stringPropertyNames()) { if(!propName.contains(".")) continue; String baseName = propName.split("\\.")[0]; if(!processed.add(baseName)) continue; String s = props.getProperty(baseName+".type"); if(s == null) { Logger.error(MainJarDependencies.class, "dependencies.properties broken? missing type for \""+baseName+"\""); continue; } final DEPENDENCY_TYPE type; try { type = DEPENDENCY_TYPE.valueOf(s); } catch (IllegalArgumentException e) { if(s.startsWith("OPTIONAL_")) { if(logMINOR) Logger.minor(MainJarDependencies.class, "Ignoring non-essential dependency type \""+s+"\" for \""+baseName+"\""); continue; } Logger.error(MainJarDependencies.class, "dependencies.properties broken? unrecognised type for \""+baseName+"\""); continue; } // Check operating system restrictions. s = props.getProperty(baseName+".os"); if(s != null) { if(!matchesCurrentOS(s)) { Logger.normal(MainJarDependenciesChecker.class, "Ignoring "+baseName+" as not relevant to this operating system"); continue; } } // Check architecture restrictions. s = props.getProperty(baseName+".arch"); if(s != null) { if(!matchesCurrentArch(s)) { Logger.normal(this, "Ignoring "+baseName+" as not relevant to this architecture"); continue; } } // For wrapper updates. // 3.2 tolerates "java" being a script, 3.5 does not, so we must not upgrade in this case. String mustBeOnPathNotAScript = props.getProperty(baseName+".mustBeOnPathNotAScript"); if(mustBeOnPathNotAScript != null && !isOnPathNotAScript(mustBeOnPathNotAScript)) { Logger.normal(this, "Ignoring "+baseName+" because needs \""+mustBeOnPathNotAScript+"\" on the path and not a script"); System.out.println( "Ignoring "+baseName+" because needs \""+mustBeOnPathNotAScript+"\" on the path and not a script"); // FIXME remove when tested continue; } if(type == DEPENDENCY_TYPE.OPTIONAL_ATOMIC_MULTI_FILES_WITH_RESTART) { parseAtomicMultiFilesWithRestart(props, baseName); continue; } // Version is useful for checking for obsolete versions of files. String version = props.getProperty(baseName+".version"); if(version == null) { Logger.error(MainJarDependencies.class, "dependencies.properties broken? missing version"); return false; } File filename = null; s = props.getProperty(baseName+".filename"); // FIXME use nodeDir if(s != null) filename = new File(s); if(filename == null) { Logger.error(MainJarDependencies.class, "dependencies.properties broken? missing filename"); return false; } final FreenetURI key; s = props.getProperty(baseName+".key"); if(s == null) { Logger.error(MainJarDependencies.class, "dependencies.properties broken? missing "+baseName+".key"); return false; } try { key = new FreenetURI(s); } catch (MalformedURLException e) { Logger.error(MainJarDependencies.class, "Unable to parse CHK for "+baseName+": \""+s+"\": "+e, e); return false; } Pattern p = null; // Regex used for matching filenames. if(type == DEPENDENCY_TYPE.CLASSPATH) { String regex = props.getProperty(baseName+".filename-regex"); if(regex == null) { Logger.error(MainJarDependencies.class, "No "+baseName+".filename-regex in dependencies.properties"); return false; } try { p = Pattern.compile(regex); } catch (PatternSyntaxException e) { Logger.error(MainJarDependencies.class, "Bogus Pattern \""+regex+"\" in dependencies.properties"); return false; } } final byte[] expectedHash = parseExpectedHash(props.getProperty(baseName+".sha256"), baseName); if(expectedHash == null) { System.err.println("Unable to update to build "+build+": dependencies.properties broken: No hash for "+baseName); return false; } s = props.getProperty(baseName+".size"); long size = -1; if(s != null) { try { size = Long.parseLong(s); } catch (NumberFormatException e) { size = -1; } } if(size < 0) { System.err.println("Unable to update to build "+build+": dependencies.properties broken: Broken length for "+baseName+" : \""+s+"\""); return false; } s = props.getProperty(baseName+".order"); if(s != null) { try { // Order is an optional field. // For most stuff we don't care. // But if it's present it must be correct! Integer.parseInt(s); } catch (NumberFormatException e) { System.err.println("Unable to update to build "+build+": dependencies.properties broken: Broken order for "+baseName+" : \""+s+"\""); continue; } } File currentFile = null; if(type == DEPENDENCY_TYPE.CLASSPATH) currentFile = getDependencyInUse(p); if(type == DEPENDENCY_TYPE.OPTIONAL_CLASSPATH_NO_UPDATE && filename.exists()) { if(filename.canRead() && filename.length() > 0) { Logger.normal(MainJarDependenciesChecker.class, "Assuming non-updated dependency file is current: "+filename); continue; } else { System.out.println("Non-updated dependency is empty?: "+filename+" - will try to fetch it"); filename.delete(); } } if(!(type == DEPENDENCY_TYPE.CLASSPATH || type == DEPENDENCY_TYPE.OPTIONAL_PRELOAD || type == DEPENDENCY_TYPE.OPTIONAL_CLASSPATH_NO_UPDATE)) { // Whitelist types to preload. // Update this if new types need to be preloaded. continue; } // Executable? boolean executable = false; s = props.getProperty(baseName+".executable"); if(s != null) { executable = Boolean.parseBoolean(s); } if(type == DEPENDENCY_TYPE.OPTIONAL_PRELOAD && filename.exists()) currentFile = filename; // Serve the file if it meets the hash in the dependencies.properties. if(currentFile != null && currentFile.exists() && validFile(currentFile, expectedHash, size, executable)) { // File is OK. if(!type.optional) { System.out.println("Will serve "+currentFile+" for UOM"); deployer.addDependency(expectedHash, currentFile); } } else if(currentFile != null && !type.optional) { // Will be dealt with during update. For now ignore it. Not safe to preload it, since it's on the classpath, whether it exists or not. System.out.println("Component "+baseName+" is using a non-standard file, we cannot serve the file "+filename+" via UOM to other nodes. Hence they may not be able to download the update from us."); } else { // Optional update, or not present in spite of being required. final File file = filename; try { System.out.println("Preloading "+filename+(type.optional ? "" : " for the next update...")); deployer.fetch(key, filename, size, expectedHash, new JarFetcherCallback() { @Override public void onSuccess() { System.out.println("Preloaded "+file+" which will be needed when we upgrade."); if(!type.optional) { System.out.println("Will serve "+file+" for UOM"); deployer.addDependency(expectedHash, file); } } @Override public void onFailure(FetchException e) { Logger.error(this, "Failed to preload "+file+" from "+key+" : "+e, e); } }, type.optional ? 0 : build, false, executable); } catch (FetchException e) { Logger.error(MainJarDependencies.class, "Failed to preload "+file+" from "+key+" : "+e, e); } } if(currentFile == null) continue; // Ignore any old versions we might have missed that were actually on the classpath. String currentFileVersion = getDependencyVersion(currentFile); if(currentFileVersion == null) continue; // If no version in the current version, no version in any other either, can't reliably detect outdated jars. E.g. freenet-ext.jar up to v29! // Now delete bogus dependencies. for(File f : listMain) { String name = f.getName().toLowerCase(); if(!p.matcher(name).matches()) continue; // Comparing File's by equals() is dodgy, e.g. ./blah != blah. So use getName(). // Even on *nix some filesystems are case insensitive. if(name.equalsIgnoreCase(currentFile.getName())) continue; if(inClasspath(name)) continue; // Paranoia! String fileVersion = getDependencyVersion(f); if(fileVersion == null) { f.delete(); System.out.println("Deleting old dependency file (no version): "+f); continue; } if(Fields.compareVersion(fileVersion, version) <= 0) { f.delete(); System.out.println("Deleting old dependency file (outdated): "+f); } // Keep newer versions. } } return true; } static final byte[] SCRIPT_HEAD; static { try { SCRIPT_HEAD = "#!".getBytes("UTF-8"); } catch(UnsupportedEncodingException e) { throw new Error(e); } } private boolean isOnPathNotAScript(String toFind) { String path = System.getenv("PATH"); // Upper case should work on both linux and Windows if(path == null) return false; String[] split = path.split(File.pathSeparator); for(String s : split) { File f = new File(s); if(f.exists() && f.isDirectory()) { f = new File(f, toFind); if(f.exists() && f.canExecute()) { if(!f.canRead()) { Logger.error(this, "On path and can execute but not read, so can't check whether it is a script?!: "+f); return false; } if(f.length() < SCRIPT_HEAD.length) { Logger.error(this, "Found "+toFind+" on path but less than "+SCRIPT_HEAD+" bytes long, so can't check whether it is a script - will the shell try the next match? We can't tell whether it is a script or not ..."); return false; // Weird! } try { FileInputStream fis = new FileInputStream(f); byte[] buf = new byte[SCRIPT_HEAD.length]; DataInputStream dis = new DataInputStream(fis); try { dis.read(buf); return !Arrays.equals(buf, SCRIPT_HEAD); } catch (IOException e) { Logger.error(this, "Unable to read "+f+" to check whether it is a script: "+e+" - disk corruption problems???", e); return false; } finally { Closer.close(fis); Closer.close(dis); } } catch (FileNotFoundException e) { // Impossible. } } } } Logger.normal(this, "Could not find "+toFind+" on the path"); return false; // Not found on the path. } enum MUST_EXIST { /** File may or may not exist */ FALSE, /** File must exist but we don't care about its content (we're going to replace it) */ TRUE, /** File must exist and have exactly the contents expected (it's a prerequisite) */ EXACT } /** Handle a request to atomically update a set of files and restart the wrapper properly, that * is, using an external script (just telling it to restart is inadequate in this case). FORMAT: * type=OPTIONAL_ATOMIC_MULTI_FILES_WITH_RESTART * os=ALL_UNIX // handled by caller * files.1.mustExist=true // do not deploy if the file did not exist previously * OR files.1.mustExist=false // create the file if it's not there * files.1.sha256=... * files.1.filename=wrapper.jar * files.1.chk=CHK@... * files.2.... * @return False if something broke. */ private boolean parseAtomicMultiFilesWithRestart(Properties props, String name) { AtomicDeployer atomicDeployer = createRestartingAtomicDeployer(name); if(atomicDeployer == null) return false; // Platform not supported? boolean nothingToDo = true; for(String propName : props.stringPropertyNames()) { String[] split = propName.split("\\."); if(split.length != 4) continue; // namefordeploy.nameforfile.filename=... // nameforfile is not necessarily the filename, which might contain . / etc. if(!split[0].equals(name)) continue; if(!split[1].equals("files")) continue; if(!split[3].equals("filename")) continue; String fileBase = name+".files."+split[2]; // Filename. File filename = null; String s = props.getProperty(fileBase+".filename"); if(s == null) break; filename = new File(s); // Key. final FreenetURI key; s = props.getProperty(fileBase+".key"); if(s == null) { Logger.error(MainJarDependencies.class, "dependencies.properties broken? missing "+fileBase+".key in atomic multi-files list"); atomicDeployer.cleanup(); return false; } try { key = new FreenetURI(s); } catch (MalformedURLException e) { Logger.error(MainJarDependencies.class, "Unable to parse CHK for multi-files replace for "+fileBase+": \""+s+"\": "+e, e); atomicDeployer.cleanup(); return false; } // Size. s = props.getProperty(fileBase+".size"); long size = -1; if(s != null) { try { size = Long.parseLong(s); } catch (NumberFormatException e) { Logger.error(MainJarDependencies.class, "Unable to parse size for multi-files replace for "+fileBase+": \""+s+"\": "+e, e); atomicDeployer.cleanup(); return false; } } // Must exist? MUST_EXIST mustExist; s = props.getProperty(fileBase+".mustExist"); if(s == null) { mustExist = MUST_EXIST.FALSE; } else { try { mustExist = MUST_EXIST.valueOf(s.toUpperCase()); } catch (IllegalArgumentException e) { Logger.error(MainJarDependencies.class, "Unable to past mustExist \""+s+"\" for "+fileBase); atomicDeployer.cleanup(); return false; } } boolean mustBeOnClassPath = false; s = props.getProperty(fileBase+".mustBeOnClassPath"); if(s != null) { mustBeOnClassPath = Boolean.parseBoolean(s); } // SHA256 hash byte[] expectedHash = parseExpectedHash(props.getProperty(fileBase+".sha256"), fileBase); if(expectedHash == null) { System.err.println("dependencies.properties multi-file replace broken: No hash for "+fileBase); atomicDeployer.cleanup(); return false; } // Executable? boolean executable = false; s = props.getProperty(fileBase+".executable"); if(s != null) { executable = Boolean.parseBoolean(s); } if(!filename.exists()) { if(mustExist != MUST_EXIST.FALSE) { System.out.println("Not running multi-file replace "+name+" : File does not exist: "+filename); atomicDeployer.cleanup(); return false; } nothingToDo = false; System.out.println("Multi-file replace: Must create "+filename+" for "+name); } else if(!validFile(filename, expectedHash, size, executable)) { if(mustExist == MUST_EXIST.EXACT) { System.out.println("Not running multi-file replace: Not compatible with old version of prerequisite "+filename); atomicDeployer.cleanup(); return false; } System.out.println("Multi-file replace: Must update "+filename+" for "+name); nothingToDo = false; } else if(mustExist == MUST_EXIST.EXACT) continue; if(mustBeOnClassPath) { File f = getDependencyInUse(Pattern.compile(Pattern.quote(filename.getName()))); if(f == null) { System.err.println("Not running multi-file replace: File must be on classpath: "+filename+" for "+name); atomicDeployer.cleanup(); return false; } } AtomicDependency dependency; try { dependency = new AtomicDependency(filename, key, size, expectedHash, executable); } catch (IOException e) { System.err.println("Unable to start multi-file update for "+name+" : "+e); atomicDeployer.cleanup(); return false; } atomicDeployer.add(dependency); } if(nothingToDo) { System.out.println("Multi-file replace: Nothing to do for "+name+"."); atomicDeployer.cleanup(); return false; // Valid no-op. } atomicDeployer.start(); return true; } static final String UPDATER_BACKUP_SUFFIX = ".update.bak.tmp"; /** A file to be replaced as part of a multi-file replace. */ private class AtomicDependency implements JarFetcherCallback { /** Temporary file to store the downloaded data in until it is ready to deploy */ private final File tempFilename; /** Temporary file to store a copy of the old file in until the deploy has succeeded */ private final File backupFilename; private final File filename; private final FreenetURI key; private final long size; private final byte[] expectedHash; private final boolean executable; private AtomicDeployer myDeployer; private JarFetcher fetcher; private boolean nothingToBackup; private boolean triedDeploy; private boolean succeededFetch; private boolean backedUp; public AtomicDependency(File filename, FreenetURI key, long size, byte[] expectedHash, boolean executable) throws IOException { this.filename = filename; this.key = key; this.size = size; this.expectedHash = expectedHash; this.executable = executable; File parent = filename.getAbsoluteFile().getParentFile(); if(parent == null) parent = new File("."); File[] list = parent.listFiles(); for(File f : list) { String name = f.getName(); if(name.startsWith(filename.getName()) && name.endsWith(UPDATER_BACKUP_SUFFIX)) f.delete(); } this.tempFilename = File.createTempFile(filename.getName(), ".tmp", parent); tempFilename.deleteOnExit(); this.backupFilename = File.createTempFile(filename.getName(), UPDATER_BACKUP_SUFFIX, parent); } public boolean start(AtomicDeployer myDeployer) { synchronized(this) { if(this.myDeployer != null) return true; // Already running. this.myDeployer = myDeployer; } System.out.println("Fetching "+filename+" from "+key); try { JarFetcher fetcher = deployer.fetch(key, tempFilename, size, expectedHash, this, build, false, executable /* we use rename, so ideally we'd like the temp file to be executable if the target will be */); synchronized(this) { this.fetcher = fetcher; } return true; } catch (FetchException e) { Logger.error(this, "Unable to start fetch for "+filename+" from "+key+" size "+size+" expected hash "+HexUtil.bytesToHex(expectedHash)+" : "+e, e); System.err.println("Unable to start fetch for "+filename+" for multi-file replace"); return false; } } @Override public void onSuccess() { AtomicDeployer d; synchronized(this) { succeededFetch = true; d = myDeployer; } System.out.println("Fetched "+filename+" from "+key); d.onSuccess(this); } @Override public void onFailure(FetchException e) { System.out.println("Failed to fetch "+filename+" from "+key); getDeployer().onFailure(this, e); } private synchronized AtomicDeployer getDeployer() { return myDeployer; } public void cancel() { JarFetcher f; synchronized(this) { f = fetcher; fetcher = null; } if(f == null) return; f.cancel(); } boolean backupOriginal() { System.out.println("Backing up "+filename+" to "+backupFilename); if(!filename.exists()) { synchronized(this) { nothingToBackup = true; backedUp = true; } return true; } if(FileUtil.copyFile(filename, backupFilename)) { synchronized(this) { backedUp = true; } if(executable) return backupFilename.setExecutable(true) || backupFilename.canExecute(); return true; } else return false; } boolean deploy() { System.out.println("Deploying "+tempFilename+" to "+filename); synchronized(this) { assert(succeededFetch); assert(backedUp); triedDeploy = true; } if(!filename.exists()) { if(tempFilename.renameTo(filename)) { if(executable) return filename.setExecutable(true) || filename.canExecute(); return true; } else return false; } else { if(tempFilename.renameTo(filename)) { if(executable) return filename.setExecutable(true) || filename.canExecute(); return true; } filename.delete(); if(tempFilename.renameTo(filename)) { if(executable) return filename.setExecutable(true) || filename.canExecute(); return true; } else return false; } } boolean revertFromBackup() { synchronized(this) { assert(succeededFetch); assert(backedUp); if(!triedDeploy) return true; // Valid no-op. } System.out.println("Reverting from backup "+backupFilename+" to "+filename); boolean nothingToBackup; synchronized(this) { nothingToBackup = this.nothingToBackup; } if(nothingToBackup) { if(!filename.delete() && filename.exists()) { System.err.println("Unable to delete file while reverting multi-file deploy: "+filename); tempFilename.delete(); return true; // Usually this is OK. } else { tempFilename.delete(); return true; } } else { if(!backupFilename.renameTo(filename)) return false; if(executable) { if(filename.setExecutable(true) || filename.canExecute()) { tempFilename.delete(); return true; } else return false; } else { tempFilename.delete(); return true; } } } void cleanup() { tempFilename.delete(); backupFilename.delete(); } } private AtomicDeployer createRestartingAtomicDeployer(String name) { if(FileUtil.detectedOS.isUnix || FileUtil.detectedOS.isMac) { return new UnixRestartingAtomicDeployer(name); } else if(FileUtil.detectedOS.isWindows) { System.out.println("Multi-file update for "+name+" not supported on Windows at present, see bug #5883"); // FIXME implement Windows support using bug #5883. return null; } else { System.out.println("Multi-file update for "+name+" not supported on unknown non-unix non-windows OS "+FileUtil.detectedOS); return null; } } /** Deploys a multi-file replace without a restart */ class AtomicDeployer { private final Set<AtomicDependency> dependencies = new HashSet<AtomicDependency>(); private final Set<AtomicDependency> dependenciesWaiting = new HashSet<AtomicDependency>(); private boolean failed; private boolean started; final String name; /** Create an AtomicDeployer, which will wait for the downloads and then deploy a * multi-file replace atomically, that is all at once. * @param name The internal name of the deployment job. For UI purposes we will simply * feed this into the localisation code. */ public AtomicDeployer(String name) { this.name = name; } public void cleanup() { for(AtomicDependency dep : dependencies()) { dep.cancel(); dep.cleanup(); } } public void onFailure(AtomicDependency dep, FetchException e) { synchronized(this) { failed = true; dependenciesWaiting.remove(dep); } System.err.println("Unable to deploy multi-file update "+name+" because fetch failed for "+dep.filename); cleanup(); } public void onSuccess(AtomicDependency dep) { synchronized(this) { assert(dependencies.contains(dep)); dependenciesWaiting.remove(dep); if(!dependenciesWaiting.isEmpty()) return; if(failed) return; } readyToDeploy(); } private void readyToDeploy() { deployer.multiFileReplaceReadyToDeploy(this); } public synchronized void add(AtomicDependency dependency) { if(started) { Logger.error(this, "Already started!"); failed = true; return; } dependencies.add(dependency); dependenciesWaiting.add(dependency); } public void start() { for(AtomicDependency dep : dependencies()) { if(!dep.start(this)) { System.err.println("Unable to start fetch for "+this); AtomicDependency[] deps; synchronized(this) { failed = true; deps = dependencies(); } for(AtomicDependency kill : deps) { kill.cancel(); } return; } } synchronized(this) { started = true; } } private synchronized AtomicDependency[] dependencies() { return dependencies.toArray(new AtomicDependency[dependencies.size()]); } public void deployMultiFileUpdateOffThread() { executor.execute(new PrioRunnable() { @Override public void run() { synchronized(NodeUpdateManager.deployLock()) { if(deployMultiFileUpdate()) NodeUpdateManager.waitForever(); } } @Override public int getPriority() { return NativeThread.MAX_PRIORITY; } }); } protected boolean deployMultiFileUpdate() { if(!innerDeployMultiFileUpdate()) { System.err.println("Failed to deploy multi-file update "+name); return false; } else return true; } /** Replace all the files or none of the files */ boolean innerDeployMultiFileUpdate() { synchronized(this) { if(failed || !started) { Logger.error(this, "Not deploying: failed="+failed+" started="+started, new Exception("error")); return false; } } AtomicDependency[] deps = dependencies(); for(AtomicDependency dep : deps) { if(!dep.backupOriginal()) { System.err.println("Unable to backup dependency "+dep.filename+" - aborting multi-file update deployment "+name); return false; } } boolean failedDeploy = false; for(AtomicDependency dep : deps) { if(!dep.deploy()) { failedDeploy = true; System.err.println("Unable to update file "+dep.filename+" from "+dep.tempFilename+" - aborting multi-file update deployment "+name); break; } } if(failedDeploy) { System.err.println("Deploying multi-file update failed: "+name); System.err.println("Restoring files from backups"); for(AtomicDependency dep : deps) { if(!dep.revertFromBackup()) { System.err.println("Restoring file from backup failed. Freenet may fail to start on next restart! You should move "+dep.backupFilename+" to "+dep.filename); // FIXME useralert??? } } } return !failedDeploy; } } /** Deploys a multi-file replace with a restart */ private abstract class RestartingAtomicDeployer extends AtomicDeployer { public RestartingAtomicDeployer(String name) { super(name); } } /** Deploys a multi-file replace on *nix with a restart, using a simple shell script */ private class UnixRestartingAtomicDeployer extends RestartingAtomicDeployer { public UnixRestartingAtomicDeployer(String name) { super(name); } @Override protected boolean deployMultiFileUpdate() { if(!WrapperManager.isControlledByNativeWrapper()) return false; File restartScript; try { restartScript = createRestartScript(); } catch (IOException e) { System.err.println("Unable to deploy multi-file update for "+name+" because cannot write script to restart the wrapper: "+e); Logger.error(this, "Unable to deploy multi-file update for "+name+" because cannot write script to restart the wrapper: "+e, e); return false; } if(restartScript == null) return false; File shell = findShell(); if(shell == null) return false; if(innerDeployMultiFileUpdate()) { try { // FIXME use nodeDir if(Runtime.getRuntime().exec(new String[] { shell.toString(), restartScript.toString() }) == null) { System.err.println("Unable to start restarter script "+restartScript+" with shell "+shell+" -> cannot deploy multi-file update for "+name); return false; } } catch (IOException e) { System.err.println("Unable to start restarter script "+restartScript+" with shell "+shell+" -> cannot deploy multi-file update for "+name+" : "+e); Logger.error(this, "Unable to start restarter script "+restartScript+" with shell "+shell+" -> cannot deploy multi-file update for "+name+" : "+e, e); return false; } System.out.println("Shutting down Freenet for hard restart after deploying multi-file update for "+name+". The script "+restartScript+" should start it back up."); WrapperManager.stop(0); return true; } else return false; } private File findShell() { File f = new File("/bin/sh"); if(f.exists() && f.canExecute()) return f; f = new File("/bin/bash"); if(f.exists() && f.canExecute()) return f; System.err.println("Unable to find system shell"); return null; } static final String RESTART_SCRIPT_NAME = "tempRestartFreenet.sh"; private File createRestartScript() throws IOException { // FIXME use nodeDir File runsh = new File("run.sh"); String runshNoNice = "run.nonice-for-update.sh"; if(!(runsh.exists() && runsh.canExecute())) { System.err.println("Cannot find run.sh so cannot deploy multi-file update for "+name); return null; } // EVIL HACK if(!createRunShNoNice(runsh, new File(runshNoNice))) { return null; } if(!new File("/dev/null").exists()) { System.err.println("Cannot deploy multi-file update for "+name+" without /dev/null"); return null; } File restartFreenet = new File(RESTART_SCRIPT_NAME); restartFreenet.delete(); FileBucket fb = new FileBucket(restartFreenet, false, true, false, false); OutputStream os = null; try { os = new BufferedOutputStream(fb.getOutputStream()); OutputStreamWriter osw = new OutputStreamWriter(os, "ISO-8859-1"); // Right??? osw.write("#!/bin/sh\n"); // FIXME exec >/dev/null 2>&1 ???? Believed to be portable. //osw.write("trap true PIPE\n"); - should not be necessary osw.write("while kill -0 "+WrapperManager.getWrapperPID()+" > /dev/null 2>&1; do sleep 1; done\n"); osw.write("./"+runshNoNice+" start > /dev/null 2>&1\n"); osw.write("rm "+RESTART_SCRIPT_NAME+"\n"); osw.write("rm "+runshNoNice+"\n"); osw.close(); osw = null; os = null; return restartFreenet; } finally { Closer.close(os); } } /** Evil hack: Rewrite run.sh so it has PRIORITY=0. * REDFLAG FIXME TODO Surely we can improve on this? This mechanism is only used for * updating very old wrapper installs - but we'll want to update the wrapper in the future * too, and the ability to restart the wrapper fully is likely useful, so maybe we won't * just get rid of this - in which case maybe we want to improve on this. * @throws IOException */ private boolean createRunShNoNice(File input, File output) throws IOException { final String charset = "UTF-8"; InputStream is = null; OutputStream os = null; boolean failed = false; try { is = new FileInputStream(input); BufferedReader br = new BufferedReader(new InputStreamReader(new BufferedInputStream(is), charset)); os = new FileOutputStream(output); Writer w = new BufferedWriter(new OutputStreamWriter(new BufferedOutputStream(os), charset)); boolean writtenPrio = false; String line; while((line = br.readLine()) != null) { if((!writtenPrio) && line.startsWith("PRIORITY=")) { writtenPrio = true; line = "PRIORITY="; // = don't use nice. } w.write(line+"\n"); } // We want to see exceptions on close() here. br.close(); is = new FileInputStream(input); w.close(); os = null; if(!(output.setExecutable(true) || output.canExecute())) { failed = true; return false; } return true; } catch (UnsupportedEncodingException e) { throw new Error(e); } catch (IOException e) { failed = true; return false; } finally { Closer.close(is); Closer.close(os); if(failed) output.delete(); } } } public static String getDependencyVersion(File currentFile) { // We can't use parseProperties because there are multiple sections. InputStream is = null; try { is = new FileInputStream(currentFile); ZipInputStream zis = new ZipInputStream(is); ZipEntry ze; while(true) { ze = zis.getNextEntry(); if(ze == null) break; if(ze.isDirectory()) continue; String name = ze.getName(); if(name.equals("META-INF/MANIFEST.MF")) { final String key = "Implementation-Version"; BufferedInputStream bis = new BufferedInputStream(zis); Manifest m = new Manifest(bis); bis.close(); bis = null; Attributes a = m.getMainAttributes(); if(a != null) { String ver = a.getValue(key); if(ver != null) return ver; } a = m.getAttributes("common"); if(a != null) { String ver = a.getValue(key); if(ver != null) return ver; } } } Logger.error(MainJarDependenciesChecker.class, "Unable to get dependency version from "+currentFile); return null; } catch (FileNotFoundException e) { return null; } catch (IOException e) { return null; } finally { Closer.close(is); } } /** Find the current filename, on the classpath, of the dependency given. * Note that this may not actually exist, and the caller should check! * However, even a non-existent filename may be useful when updating * wrapper.conf. */ private static File getDependencyInUse(Pattern p) { if(p == null) return null; // Optional in some cases. String classpath = System.getProperty("java.class.path"); String[] split = classpath.split(File.pathSeparator); for(String s : split) { File f = new File(s); if(p.matcher(f.getName().toLowerCase()).matches()) return f; } return null; } private static boolean inClasspath(String name) { String classpath = System.getProperty("java.class.path"); String[] split = classpath.split(File.pathSeparator); for(String s : split) { File f = new File(s); if(name.equalsIgnoreCase(f.getName())) return true; } return false; } private static byte[] parseExpectedHash(String sha256, String baseName) { if(sha256 == null) { Logger.error(MainJarDependencies.class, "No SHA256 for "+baseName+" in dependencies.properties"); return null; } try { return HexUtil.hexToBytes(sha256); } catch (NumberFormatException e) { Logger.error(MainJarDependencies.class, "Bogus expected hash: \""+sha256+"\" : "+e, e); return null; } catch (IndexOutOfBoundsException e) { Logger.error(MainJarDependencies.class, "Bogus expected hash: \""+sha256+"\" : "+e, e); return null; } } public static boolean validFile(File filename, byte[] expectedHash, long size, boolean executable) { if(filename == null) return false; if(!filename.exists()) return false; if(filename.length() != size) { System.out.println("File exists while updating but length is wrong ("+filename.length()+" should be "+size+") for "+filename); return false; } FileInputStream fis = null; try { fis = new FileInputStream(filename); MessageDigest md = SHA256.getMessageDigest(); SHA256.hash(fis, md); byte[] hash = md.digest(); SHA256.returnMessageDigest(md); fis.close(); fis = null; if(Arrays.equals(hash, expectedHash)) { if(executable && !filename.canExecute()) { filename.setExecutable(true); } return true; } else { return false; } } catch (FileNotFoundException e) { Logger.error(MainJarDependencies.class, "File not found: "+filename); return false; } catch (IOException e) { System.err.println("Unable to read "+filename+" for updater"); return false; } finally { Closer.close(fis); } } private synchronized void clear(int build) { dependencies.clear(); broken = false; this.build = build; final Downloader[] toCancel = downloaders.toArray(new Downloader[downloaders.size()]); executor.execute(new Runnable() { @Override public void run() { for(Downloader d : toCancel) d.cancel(); } }); downloaders.clear(); } /** Unlike other methods here, this should be called outside the lock. */ public void deploy() { TreeSet<Dependency> f; synchronized(this) { f = new TreeSet<Dependency>(dependencies); } if(logMINOR) Logger.minor(this, "Deploying build "+build+" with "+f.size()+" dependencies"); deployer.deploy(new MainJarDependencies(f, build)); } private synchronized void fetchDependency(FreenetURI chk, Dependency dep, byte[] expectedHash, long expectedSize, boolean essential, boolean executable) throws FetchException { Downloader d = new Downloader(dep, chk, expectedHash, expectedSize, essential, executable, build); if(essential) downloaders.add(d); } private synchronized boolean ready() { if(broken) return false; if(!downloaders.isEmpty()) return false; return true; } public synchronized boolean isBroken() { return broken; } }