/* 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;
}
}