package freenet.node.updater;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Properties;
import freenet.client.FetchContext;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.FetchResult;
import freenet.client.async.ClientContext;
import freenet.client.async.ClientGetCallback;
import freenet.client.async.ClientGetter;
import freenet.client.events.ClientEvent;
import freenet.client.events.ClientEventListener;
import freenet.client.events.SplitfileProgressEvent;
import freenet.clients.fcp.FCPMessage;
import freenet.clients.fcp.ClientPut.COMPRESS_STATE;
import freenet.clients.http.QueueToadlet;
import freenet.keys.FreenetURI;
import freenet.l10n.NodeL10n;
import freenet.node.RequestClient;
import freenet.node.RequestStarter;
import freenet.node.Version;
import freenet.node.updater.MainJarDependenciesChecker.AtomicDeployer;
import freenet.node.updater.MainJarDependenciesChecker.Deployer;
import freenet.node.updater.MainJarDependenciesChecker.JarFetcher;
import freenet.node.updater.MainJarDependenciesChecker.JarFetcherCallback;
import freenet.node.updater.MainJarDependenciesChecker.MainJarDependencies;
import freenet.node.updater.UpdateOverMandatoryManager.UOMDependencyFetcher;
import freenet.node.updater.UpdateOverMandatoryManager.UOMDependencyFetcherCallback;
import freenet.node.useralerts.UserAlert;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.io.Closer;
import freenet.support.io.FileBucket;
import freenet.support.io.FileUtil;
import freenet.support.io.InsufficientDiskSpaceException;
public class MainJarUpdater extends NodeUpdater implements Deployer {
static private volatile boolean logMINOR;
static {
Logger.registerClass(MainJarUpdater.class);
}
private final FetchContext dependencyCtx;
private final ClientContext clientContext;
MainJarUpdater(NodeUpdateManager manager, FreenetURI URI, int current, int min, int max, String blobFilenamePrefix) {
super(manager, URI, current, min, max, blobFilenamePrefix);
dependencyCtx = core.makeClient((short) 0, true, false).getFetchContext();
dependencyCtx.allowSplitfiles = true;
dependencyCtx.dontEnterImplicitArchives = false;
dependencyCtx.maxNonSplitfileRetries = -1;
dependencyCtx.maxSplitfileBlockRetries = -1;
clientContext = core.clientContext;
dependencies = new MainJarDependenciesChecker(this, manager.node.executor);
}
private final MainJarDependenciesChecker dependencies;
@Override
public String jarName() {
return "freenet.jar";
}
public void start() {
maybeProcessOldBlob();
super.start();
}
@Override
protected void maybeParseManifest(FetchResult result, int build) {
// Do nothing.
}
@Override
protected void processSuccess(int fetched, FetchResult result, File blob) {
manager.onDownloadedNewJar(result.asBucket(), fetched, blob);
// NodeUpdateManager expects us to dependencies *AFTER* we tell it about the new jar.
parseDependencies(result, fetched);
}
@Override
protected void onStartFetching() {
manager.onStartFetching();
}
// Dependency handling.
private HashSet<DependencyJarFetcher> fetchers = new HashSet<DependencyJarFetcher>();
private HashSet<DependencyJarFetcher> essentialFetchers = new HashSet<DependencyJarFetcher>();
protected void parseDependencies(Properties props, int build) {
synchronized(fetchers) {
fetchers.clear();
}
MainJarDependencies deps = dependencies.handle(props, build);
if(deps != null) {
manager.onDependenciesReady(deps);
}
}
@Override
public void deploy(MainJarDependencies deps) {
manager.onDependenciesReady(deps);
}
/** Glue code. */
private class DependencyJarFetcher implements JarFetcher, ClientGetCallback, RequestClient, ClientEventListener {
private final File filename;
private final ClientGetter getter;
private SplitfileProgressEvent lastProgress;
private final JarFetcherCallback cb;
private boolean fetched;
private final byte[] expectedHash;
private final long expectedLength;
private final boolean essential;
private final File tempFile;
private UOMDependencyFetcher uomFetcher;
private final boolean executable;
DependencyJarFetcher(File filename, FreenetURI chk, long expectedLength, byte[] expectedHash, JarFetcherCallback cb, boolean essential, boolean executable) throws FetchException {
FetchContext myCtx = new FetchContext(dependencyCtx, FetchContext.IDENTICAL_MASK);
File parent = filename.getParentFile();
if(parent == null) parent = new File(".");
try {
tempFile = File.createTempFile(filename.getName(), NodeUpdateManager.TEMP_FILE_SUFFIX, parent);
} catch (InsufficientDiskSpaceException e) {
throw new FetchException(FetchExceptionMode.NOT_ENOUGH_DISK_SPACE);
} catch (IOException e) {
throw new FetchException(FetchExceptionMode.BUCKET_ERROR, "Cannot create temp file for "+filename+" in "+parent+" - disk full? permissions problem?");
}
getter = new ClientGetter(this,
chk, myCtx, RequestStarter.IMMEDIATE_SPLITFILE_PRIORITY_CLASS,
new FileBucket(tempFile, false, false, false, false), null, null);
myCtx.eventProducer.addEventListener(this);
this.cb = cb;
this.filename = filename;
this.expectedHash = expectedHash;
this.expectedLength = expectedLength;
this.essential = essential;
this.executable = executable;
}
@Override
public void cancel() {
final UOMDependencyFetcher f;
synchronized(this) {
fetched = true;
f = uomFetcher;
}
MainJarUpdater.this.node.executor.execute(new Runnable() {
@Override
public void run() {
getter.cancel(clientContext);
if(f != null) f.cancel();
}
});
}
@Override
public boolean persistent() {
return false;
}
@Override
public boolean realTimeFlag() {
return false;
}
@Override
public void onSuccess(FetchResult result, ClientGetter state) {
synchronized(this) {
if(fetched) {
tempFile.delete();
return;
}
fetched = true;
}
if(!MainJarDependenciesChecker.validFile(tempFile, expectedHash, expectedLength, executable)) {
Logger.error(this, "Unable to download dependency "+filename+" : not the expected size or hash!");
System.err.println("Download of "+filename+" for update failed because temp file appears to be corrupted!");
if(cb != null)
cb.onFailure(new FetchException(FetchExceptionMode.BUCKET_ERROR, "Downloaded jar from Freenet but failed consistency check: "+tempFile+" length "+tempFile.length()+" "));
tempFile.delete();
return;
}
if(!FileUtil.renameTo(tempFile, filename)) {
Logger.error(this, "Unable to rename temp file "+tempFile+" to "+filename);
System.err.println("Download of "+filename+" for update failed because cannot rename from "+tempFile);
if(cb != null)
cb.onFailure(new FetchException(FetchExceptionMode.BUCKET_ERROR, "Unable to rename temp file "+tempFile+" to "+filename));
tempFile.delete();
return;
}
if(cb != null) cb.onSuccess();
}
@Override
public void onFailure(FetchException e, ClientGetter state) {
tempFile.delete();
synchronized(this) {
if(fetched) return;
}
if(cb != null) cb.onFailure(e);
}
@Override
public synchronized void receive(ClientEvent ce, ClientContext context) {
if(ce instanceof SplitfileProgressEvent)
lastProgress = (SplitfileProgressEvent) ce;
}
private void start() throws FetchException {
getter.start(clientContext);
}
public synchronized HTMLNode renderRow() {
HTMLNode row = new HTMLNode("tr");
row.addChild("td").addChild("p", filename.toString());
if(uomFetcher != null)
row.addChild("td").addChild("#", l10n("fetchingFromUOM"));
else if(lastProgress == null)
row.addChild(QueueToadlet.createProgressCell(false, true, COMPRESS_STATE.WORKING, 0, 0, 0, 0, 0, false, false));
else
row.addChild(QueueToadlet.createProgressCell(false,
true, COMPRESS_STATE.WORKING, lastProgress.succeedBlocks, lastProgress.failedBlocks, lastProgress.fatallyFailedBlocks, lastProgress.minSuccessfulBlocks, lastProgress.totalBlocks, lastProgress.finalizedTotal, false));
return row;
}
public void fetchFromUOM() {
synchronized(this) {
if(fetched) return;
if(!essential) return;
}
UOMDependencyFetcher f = manager.uom.fetchDependency(expectedHash, expectedLength, filename, executable,
new UOMDependencyFetcherCallback() {
@Override
public void onSuccess() {
synchronized(DependencyJarFetcher.this) {
if(fetched) return;
fetched = true;
}
if(cb != null) cb.onSuccess();
}
});
synchronized(this) {
if(uomFetcher != null) {
Logger.error(this, "Started UOMFetcher twice for "+filename, new Exception("error"));
return;
}
uomFetcher = f;
}
}
@Override
public void onResume(ClientContext context) {
// Do nothing. Not persistent.
}
@Override
public RequestClient getRequestClient() {
return this;
}
}
@Override
public JarFetcher fetch(FreenetURI uri, File downloadTo,
long expectedLength, byte[] expectedHash, JarFetcherCallback cb, int build, boolean essential, boolean executable) throws FetchException {
if(essential)
System.out.println("Fetching "+downloadTo+" needed for new Freenet update "+build);
else if(build != 0) // build 0 means it's a preload or a multi-file update.
System.out.println("Preloading "+downloadTo+" needed for new Freenet update "+build);
if(logMINOR) Logger.minor(this, "Fetching "+uri+" to "+downloadTo+" for next update");
DependencyJarFetcher fetcher = new DependencyJarFetcher(downloadTo, uri, expectedLength, expectedHash, cb, essential, executable);
synchronized(fetchers) {
fetchers.add(fetcher);
if(essential)
essentialFetchers.add(fetcher);
}
fetcher.start();
if(manager.uom.fetchingUOM()) {
if(essential)
fetcher.fetchFromUOM();
}
return fetcher;
}
public void onStartFetchingUOM() {
DependencyJarFetcher[] f;
synchronized(fetchers) {
f = fetchers.toArray(new DependencyJarFetcher[fetchers.size()]);
}
for(DependencyJarFetcher fetcher : f)
fetcher.fetchFromUOM();
}
public void renderProperties(HTMLNode alertNode) {
synchronized(fetchers) {
if(!fetchers.isEmpty()) {
alertNode.addChild("p", l10n("fetchingDependencies")+":");
HTMLNode table = alertNode.addChild("table");
for(DependencyJarFetcher f : fetchers) {
table.addChild(f.renderRow());
}
}
}
}
private String l10n(String key) {
return NodeL10n.getBase().getString("MainJarUpdater."+key);
}
public boolean brokenDependencies() {
return dependencies.isBroken();
}
public void cleanupDependencies() {
InputStream is = getClass().getResourceAsStream("/"+DEPENDENCIES_FILE);
if(is == null) {
System.err.println("Can't find dependencies file. Other nodes will not be able to use Update Over Mandatory through this one.");
return;
}
Properties props = new Properties();
try {
props.load(is);
} catch (IOException e) {
System.err.println("Can't read dependencies file. Other nodes will not be able to use Update Over Mandatory through this one.");
return;
} finally {
Closer.close(is);
}
dependencies.cleanup(props, this, Version.buildNumber());
}
@Override
public void addDependency(byte[] expectedHash, File filename) {
manager.uom.addDependency(expectedHash, filename);
}
@Override
public void reannounce() {
this.manager.broadcastUOMAnnouncesNew();
this.manager.broadcastUOMAnnouncesOld();
}
@Override
public void multiFileReplaceReadyToDeploy(final MainJarDependenciesChecker.AtomicDeployer atomicDeployer) {
if(this.manager.isAutoUpdateAllowed()) {
atomicDeployer.deployMultiFileUpdateOffThread();
} else {
final long now = System.currentTimeMillis();
System.err.println("Not deploying multi-file update for "+atomicDeployer.name+" because auto-update is not enabled.");
node.clientCore.alerts.register(new UserAlert() {
private String l10n(String key) {
return NodeL10n.getBase().getString("MainJarUpdater.ConfirmMultiFileUpdater."+key);
}
@Override
public boolean userCanDismiss() {
return true;
}
@Override
public String getTitle() {
return l10n("title."+atomicDeployer.name);
}
@Override
public String getText() {
return l10n("text."+atomicDeployer.name);
}
@Override
public HTMLNode getHTMLText() {
return new HTMLNode("p", getText());
// FIXME separate button, then the alert could be dismissable? Only useful if it's permanently dismissable though, which means a config setting as well...
}
@Override
public String getShortText() {
return getTitle();
}
@Override
public short getPriorityClass() {
return UserAlert.ERROR;
}
@Override
public boolean isValid() {
return true;
}
@Override
public void isValid(boolean validity) {
// Ignore
}
@Override
public String dismissButtonText() {
return NodeL10n.getBase().getDefaultString("UpdatedVersionAvailableUserAlert.updateNowButton");
}
@Override
public boolean shouldUnregisterOnDismiss() {
return true;
}
@Override
public void onDismiss() {
atomicDeployer.deployMultiFileUpdateOffThread();
}
@Override
public String anchor() {
return "multi-file-update-confirm-"+atomicDeployer.name;
}
@Override
public boolean isEventNotification() {
return false;
}
@Override
public FCPMessage getFCPMessage() {
return null;
}
@Override
public long getUpdatedTime() {
return now;
}
});
}
}
}