package freenet.node.updater;

import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.HashMap;
import java.util.Map;

import freenet.client.FetchContext;
import freenet.client.FetchException;
import freenet.client.FetchResult;
import freenet.client.HighLevelSimpleClient;
import freenet.client.async.ClientContext;
import freenet.client.async.ClientGetCallback;
import freenet.client.async.ClientGetter;
import freenet.client.async.PersistenceDisabledException;
import freenet.config.Config;
import freenet.config.InvalidConfigValueException;
import freenet.config.NodeNeedRestartException;
import freenet.config.SubConfig;
import freenet.io.comm.ByteCounter;
import freenet.io.comm.DMT;
import freenet.io.comm.Message;
import freenet.io.comm.NotConnectedException;
import freenet.keys.FreenetURI;
import freenet.l10n.NodeL10n;
import freenet.node.Node;
import freenet.node.NodeFile;
import freenet.node.NodeInitException;
import freenet.node.NodeStarter;
import freenet.node.OpennetManager;
import freenet.node.PeerNode;
import freenet.node.ProgramDirectory;
import freenet.node.RequestClient;
import freenet.node.RequestStarter;
import freenet.node.Version;
import freenet.node.updater.MainJarDependenciesChecker.MainJarDependencies;
import freenet.node.updater.UpdateDeployContext.UpdateCatastropheException;
import freenet.node.useralerts.RevocationKeyFoundUserAlert;
import freenet.node.useralerts.SimpleUserAlert;
import freenet.node.useralerts.UpdatedVersionAvailableUserAlert;
import freenet.node.useralerts.UserAlert;
import freenet.pluginmanager.OfficialPlugins.OfficialPluginDescription;
import freenet.pluginmanager.PluginInfoWrapper;
import freenet.support.HTMLNode;
import freenet.support.JVMVersion;
import freenet.support.Logger;
import freenet.support.api.BooleanCallback;
import freenet.support.api.Bucket;
import freenet.support.api.StringCallback;
import freenet.support.io.BucketTools;
import freenet.support.io.Closer;
import freenet.support.io.FileUtil;

/**
 * <p>Supervises NodeUpdater's. Enables us to easily update multiple files, change
 * the URI's on the fly, eliminates some messy code in the callbacks etc.</p>
 *
 * <p>Procedure for updating the update key: Create a new key. Create a new build X, the
 * "transition version". This must be UOM-compatible with the previous transition version.
 * UOM-compatible means UOM should work from the older builds. This in turn means that it should
 * support an overlapping set of connection setup negTypes (@link
 * FNPPacketMangler.supportedNegTypes()). Similarly there may be issues with changes to the UOM
 * messages, or to messages in general. Build X is inserted to both the old key and the new key.
 * Build X's SSK URI (on the old auto-update key) will be hard-coded as the new transition version.
 * Then the next build, X+1, can get rid of some of the back compatibility cruft (especially old
 * connection setup types), and will be inserted only to the new key. Secure backups of the new
 * key are required and are documented elsewhere.</p>
 *
 * FIXME: See bug #6009 for some current UOM compatibility issues.
 */
public class NodeUpdateManager {

	/**
	 * The last build on the previous key with Java 7 support. Older nodes can
	 * update to this point via old UOM.
	 */
	public final static int TRANSITION_VERSION = 1481;

	/** The URI for post-TRANSITION_VERSION builds' freenet.jar on modern JVMs. */
	public final static String UPDATE_URI = "USK@vCKGjQtKuticcaZ-dwOgmkYPVLj~N1dm9mb3j3Smg4Y,-wz5IYtd7PlhI2Kx4cAwpUu13fW~XBglPyOn8wABn60,AQACAAE/jar/"
			+ Version.buildNumber();

	/** The URI for post-TRANSITION_VERSION builds' freenet.jar on EoL JVMs. */
	public final static String LEGACY_UPDATE_URI = "SSK@ugWS2VICgMcQ5ptmEE1mAvHgUn2OSCOogJIUAvbL090,ZKO1pZRI9oaBuBQuWFL4bK3K0blvmEdqYgiIJF5GcjQ,AQACAAE/jar-"
			+ TRANSITION_VERSION;

	/**
	 * The URI for freenet.jar before the updater was rekeyed. Unless both the EoL and modern keys rekey this is
	 * LEGACY_UPDATE_URI.
	 */
	public final static String PREVIOUS_UPDATE_URI = "SSK@O~UmMwTeDcyDIW-NsobFBoEicdQcogw7yrLO2H-sJ5Y,JVU4L7m9mNppkd21UNOCzRHKuiTucd6Ldw8vylBOe5o,AQACAAE/jar-"
			+ TRANSITION_VERSION;

	public final static String REVOCATION_URI = "SSK@tHlY8BK2KFB7JiO2bgeAw~e4sWU43YdJ6kmn73gjrIw,DnQzl0BYed15V8WQn~eRJxxIA-yADuI8XW7mnzEbut8,AQACAAE/revoked";
	// These are necessary to prevent DoS.
	public static final long MAX_REVOCATION_KEY_LENGTH = 32 * 1024;
	public static final long MAX_REVOCATION_KEY_TEMP_LENGTH = 64 * 1024;
	public static final long MAX_REVOCATION_KEY_BLOB_LENGTH = 128 * 1024;
	public static final long MAX_MAIN_JAR_LENGTH = 48 * 1024 * 1024; // 48MiB
	public static final long MAX_JAVA_INSTALLER_LENGTH = 300 * 1024 * 1024;
	public static final long MAX_WINDOWS_INSTALLER_LENGTH = 300 * 1024 * 1024;
	public static final long MAX_IP_TO_COUNTRY_LENGTH = 24 * 1024 * 1024;
	public static final long MAX_SEEDNODES_LENGTH = 3 * 1024 * 1024;

	static final FreenetURI legacyMainJarSSK;
	static final FreenetURI legacyMainJarUSK;

	static final FreenetURI previousMainJarSSK;
	static final FreenetURI previousMainJarUSK;

	public static final String transitionMainJarFilename = "legacy-freenet-jar-"
			+ TRANSITION_VERSION + ".fblob";

	public final File transitionMainJarFile;

	static {
		try {
			legacyMainJarSSK = new FreenetURI(LEGACY_UPDATE_URI);
			legacyMainJarUSK = legacyMainJarSSK.uskForSSK();
			previousMainJarSSK = new FreenetURI(PREVIOUS_UPDATE_URI);
			previousMainJarUSK = previousMainJarSSK.uskForSSK();
		} catch (MalformedURLException e) {
			throw new Error(e);
		}
	}

	private FreenetURI updateURI;
	private FreenetURI revocationURI;

	private final LegacyJarFetcher transitionMainJarFetcher;

	private MainJarUpdater mainUpdater;

	private Map<String, PluginJarUpdater> pluginUpdaters;

	private boolean autoDeployPluginsOnRestart;
	private final boolean wasEnabledOnStartup;
	/** Is auto-update enabled? */
	private volatile boolean isAutoUpdateAllowed;
	/** Has the user given the go-ahead? */
	private volatile boolean armed;
	/** Currently deploying an update? Set when we start to deploy an update.
	 * Which means it should not be un-set, except in the case of a severe
	 * error causing a valid update to fail. However, it is un-set in this
	 * case, so that we can try again with another build. */
	private boolean isDeployingUpdate;
	private final Object broadcastUOMAnnouncesSync = new Object();
	private boolean broadcastUOMAnnouncesOld = false;
	private boolean broadcastUOMAnnouncesNew = false;

	/**
	 * @deprecated Use {@link #getNode()} instead of accessing this directly.
	 */
	@Deprecated
	/* It’s not the field that is deprecated but accessing it directly is. */
	public final Node node;

	/**
	 * @deprecated Use {@link #getRevocationChecker()} instead of accessing this directly.
	 */
	@Deprecated
	/* It’s not the field that is deprecated but accessing it directly is. */
	final RevocationChecker revocationChecker;
	private String revocationMessage;
	private volatile boolean hasBeenBlown;
	private volatile boolean peersSayBlown;
	private boolean updateSeednodes;
	private boolean updateInstallers;
	// FIXME make configurable
	private boolean updateIPToCountry = true;

	/** Is there a new main jar ready to deploy? */
	private volatile boolean hasNewMainJar;
	/** If another main jar is being fetched, when did the fetch start? */
	private long startedFetchingNextMainJar;
	/** Time when we got the jar */
	private long gotJarTime;

	// Revocation alert
	private RevocationKeyFoundUserAlert revocationAlert;
	// Update alert
	private final UpdatedVersionAvailableUserAlert alert;

	/**
	 * @deprecated Use {@link #getUpdateOverMandatory()} instead of accessing this directly.
	 */
	@Deprecated
	/* It’s not the field that is deprecated but accessing it directly is. */
	public final UpdateOverMandatoryManager uom;

	private static volatile boolean logMINOR;
	private boolean disabledThisSession;

	private MainJarDependencies latestMainJarDependencies;
	private int dependenciesValidForBuild;

	/** The version we have fetched and will deploy. */
	private int fetchedMainJarVersion;
	/** The jar of the version we have fetched and will deploy. */
	private Bucket fetchedMainJarData;

	/** The blob file for the current version, for UOM */
	private File currentVersionBlobFile;

	/**
	 * The version we have fetched and aren't using because we are already
	 * deploying.
	 */
	private int maybeNextMainJarVersion;
	/**
	 * The version we have fetched and aren't using because we are already
	 * deploying.
	 */
	private Bucket maybeNextMainJarData;

	private static final Object deployLock = new Object();

	static final String TEMP_BLOB_SUFFIX = ".updater.fblob.tmp";
	static final String TEMP_FILE_SUFFIX = ".updater.tmp";

	static {
		Logger.registerClass(NodeUpdateManager.class);
	}

	public NodeUpdateManager(Node node, Config config)
			throws InvalidConfigValueException {
		this.node = node;
		this.hasBeenBlown = false;
		this.alert = new UpdatedVersionAvailableUserAlert(this);
		alert.isValid(false);

		SubConfig updaterConfig = config.createSubConfig("node.updater");

		updaterConfig.register("enabled", true, 1, false, false,
				"NodeUpdateManager.enabled", "NodeUpdateManager.enabledLong",
				new UpdaterEnabledCallback());

		wasEnabledOnStartup = updaterConfig.getBoolean("enabled");

		// is the auto-update allowed ?
		updaterConfig.register("autoupdate", false, 2, false, true,
				"NodeUpdateManager.installNewVersions",
				"NodeUpdateManager.installNewVersionsLong",
				new AutoUpdateAllowedCallback());
		isAutoUpdateAllowed = updaterConfig.getBoolean("autoupdate");

		// Set default update URI for new nodes depending on JVM version.
		updaterConfig
				.register("URI", JVMVersion.needsLegacyUpdater() ? legacyMainJarUSK.toString() : UPDATE_URI,
				          3, true, true,
						"NodeUpdateManager.updateURI",
						"NodeUpdateManager.updateURILong",
						new UpdateURICallback());

		try {
			updateURI = new FreenetURI(updaterConfig.getString("URI"));
		} catch (MalformedURLException e) {
			throw new InvalidConfigValueException(l10n("invalidUpdateURI",
					"error", e.getLocalizedMessage()));
		}

		/*
		 * The update URI is always written, so override the existing key depending on JVM version.
		 * Only override official URIs to avoid interfering with unofficial update keys.
		 *
		 * An up-to-date JVM must update the legacy URI (in addition to the previous URI) in case a node was
		 * run with an EoL JVM that was subsequently upgraded.
		 */
		if (JVMVersion.needsLegacyUpdater()) {
			transitionKey(updaterConfig, previousMainJarSSK, legacyMainJarUSK.toString());
		} else {
			transitionKey(updaterConfig, previousMainJarSSK, UPDATE_URI);
			transitionKey(updaterConfig, legacyMainJarSSK, UPDATE_URI);
		}

		updateURI = updateURI.setSuggestedEdition(Version.buildNumber());
		if(updateURI.hasMetaStrings())
			throw new InvalidConfigValueException(l10n("updateURIMustHaveNoMetaStrings"));
		if(!updateURI.isUSK())
			throw new InvalidConfigValueException(l10n("updateURIMustBeAUSK"));

		updaterConfig.register("revocationURI", REVOCATION_URI, 4, true, false,
				"NodeUpdateManager.revocationURI",
				"NodeUpdateManager.revocationURILong",
				new UpdateRevocationURICallback());

		try {
			revocationURI = new FreenetURI(
					updaterConfig.getString("revocationURI"));
		} catch (MalformedURLException e) {
			throw new InvalidConfigValueException(l10n("invalidRevocationURI",
					"error", e.getLocalizedMessage()));
		}

		LegacyJarFetcher.LegacyFetchCallback legacyFetcherCallback = new LegacyJarFetcher.LegacyFetchCallback() {

			@Override
			public void onSuccess(LegacyJarFetcher fetcher) {
				if (transitionMainJarFetcher.fetched()) {
					System.out.println("Got legacy jar, announcing...");
					broadcastUOMAnnouncesOld();
				}
			}

			@Override
			public void onFailure(FetchException e, LegacyJarFetcher fetcher) {
				Logger.error(
						this,
						"Failed to fetch "
								+ fetcher.saveTo
								+ " : UPDATE OVER MANDATORY WILL NOT WORK WITH OLDER NODES THAN "
								+ TRANSITION_VERSION + " : " + e, e);
				System.err
						.println("Failed to fetch "
								+ fetcher.saveTo
								+ " : UPDATE OVER MANDATORY WILL NOT WORK WITH OLDER NODES THAN "
								+ TRANSITION_VERSION + " : " + e);
			}

		};

		transitionMainJarFile = new File(node.getClientCore().getPersistentTempDir(), transitionMainJarFilename);
		transitionMainJarFetcher = new LegacyJarFetcher(previousMainJarSSK,
				transitionMainJarFile, node.getClientCore(),
				legacyFetcherCallback);

		updaterConfig.register("updateSeednodes", wasEnabledOnStartup, 6, true,
				true, "NodeUpdateManager.updateSeednodes",
				"NodeUpdateManager.updateSeednodesLong", new BooleanCallback() {

					@Override
					public Boolean get() {
						return updateSeednodes;
					}

					@Override
					public void set(Boolean val)
							throws InvalidConfigValueException,
							NodeNeedRestartException {
						if (updateSeednodes == val)
							return;
						updateSeednodes = val;
						if (val)
							throw new NodeNeedRestartException(
									"Must restart to fetch the seednodes");
						else
							throw new NodeNeedRestartException(
									"Must restart to stop the seednodes fetch if it is still running");
					}

				});

		updateSeednodes = updaterConfig.getBoolean("updateSeednodes");

		updaterConfig.register("updateInstallers", wasEnabledOnStartup, 6,
				true, true, "NodeUpdateManager.updateInstallers",
				"NodeUpdateManager.updateInstallersLong",
				new BooleanCallback() {

					@Override
					public Boolean get() {
						return updateInstallers;
					}

					@Override
					public void set(Boolean val)
							throws InvalidConfigValueException,
							NodeNeedRestartException {
						if (updateInstallers == val)
							return;
						updateInstallers = val;
						if (val)
							throw new NodeNeedRestartException(
									"Must restart to fetch the installers");
						else
							throw new NodeNeedRestartException(
									"Must restart to stop the installers fetches if they are still running");
					}

				});

		updateInstallers = updaterConfig.getBoolean("updateInstallers");

		updaterConfig.finishedInitialization();

		this.revocationChecker = new RevocationChecker(this, new File(
				node.getClientCore().getPersistentTempDir(), "revocation-key.fblob"));

		this.uom = new UpdateOverMandatoryManager(this);
		this.uom.removeOldTempFiles();
	}

	private void transitionKey(SubConfig updaterConfig, FreenetURI from, String to)
			throws InvalidConfigValueException {

		if (updateURI.equalsKeypair(from)) {
			try {
				updaterConfig.set("URI", to);
			} catch (NodeNeedRestartException e) {
				// UpdateURICallback.set() does not throw NodeNeedRestartException.
				Logger.warning(this, "Unexpected failure setting update URI", e);
			}
		}
	}

	class SimplePuller implements ClientGetCallback {

		final FreenetURI freenetURI;
		final String filename;
		final ProgramDirectory directory;

		public SimplePuller(FreenetURI freenetURI, NodeFile file) {
		    this(freenetURI, file.getFilename(), file.getProgramDirectory(node));
		}

		private SimplePuller(FreenetURI freenetURI, String filename, ProgramDirectory directory) {
			this.freenetURI = freenetURI;
			this.filename = filename;
			this.directory = directory;
		}

		public void start(short priority, long maxSize) {
			HighLevelSimpleClient hlsc = node.getClientCore().makeClient(priority,
					false, false);
			FetchContext context = hlsc.getFetchContext();
			context.maxNonSplitfileRetries = -1;
			context.maxSplitfileBlockRetries = -1;
			context.maxTempLength = maxSize;
			context.maxOutputLength = maxSize;
			ClientGetter get = new ClientGetter(this, freenetURI, context,
					priority, null, null, null);
			try {
				node.getClientCore().getClientContext().start(get);
			} catch (PersistenceDisabledException e) {
				// Impossible
			} catch (FetchException e) {
				onFailure(e, null);
			}
		}

		@Override
		public void onFailure(FetchException e, ClientGetter state) {
			System.err.println("Failed to fetch " + filename + " : " + e);
		}

		@Override
		public void onSuccess(FetchResult result, ClientGetter state) {
			File temp;
			FileOutputStream fos = null;
			try {
				temp = FileUtil.createTempFile(filename, ".tmp", directory.dir());
				temp.deleteOnExit();
				fos = new FileOutputStream(temp);
				BucketTools.copyTo(result.asBucket(), fos, -1);
				fos.close();
				fos = null;
				for (int i = 0; i < 10; i++) {
					// FIXME add a callback in case it's being used on Windows.
					if (FileUtil.moveTo(temp, directory.file(filename))) {
						System.out.println("Successfully fetched " + filename
								+ " for version " + Version.buildNumber());
						break;
					} else {
						System.out
								.println("Failed to rename " + temp + " to "
										+ filename
										+ " after fetching it from Freenet.");
						try {
							Thread.sleep(SECONDS.toMillis(1) + node.getFastWeakRandom().nextInt((int) SECONDS.toMillis((long) Math.min(Math.pow(2, i), MINUTES.toSeconds(15)))));
						} catch (InterruptedException e) {
							// Ignore
						}
					}
				}
				temp.delete();
			} catch (IOException e) {
				System.err
						.println("Fetched but failed to write out "
								+ filename
								+ " - please check that the node has permissions to write in "
								+ directory.dir()
								+ " and particularly the file " + filename);
				System.err.println("The error was: " + e);
				e.printStackTrace();
			} finally {
				Closer.close(fos);
				Closer.close(result.asBucket());
			}
		}

        @Override
        public void onResume(ClientContext context) {
            // Not persistent.
        }

        @Override
        public RequestClient getRequestClient() {
            return node.getNonPersistentClientBulk();
        }

	}

	public File getInstallerWindows() {
		File f = NodeFile.InstallerWindows.getFile(node);
		if (!(f.exists() && f.canRead() && f.length() > 0))
			return null;
		else
			return f;
	}

	public File getInstallerNonWindows() {
		File f = NodeFile.InstallerNonWindows.getFile(node);
		if (!(f.exists() && f.canRead() && f.length() > 0))
			return null;
		else
			return f;
	}

	public FreenetURI getSeednodesURI() {
		return updateURI.sskForUSK().setDocName(
				"seednodes-" + Version.buildNumber());
	}

	public FreenetURI getInstallerNonWindowsURI() {
		return updateURI.sskForUSK().setDocName(
				"installer-" + Version.buildNumber());
	}

	public FreenetURI getInstallerWindowsURI() {
		return updateURI.sskForUSK().setDocName(
				"wininstaller-" + Version.buildNumber());
	}

	public FreenetURI getIPv4ToCountryURI() {
		return updateURI.sskForUSK().setDocName(
				"iptocountryv4-" + Version.buildNumber());
	}

	public void start() throws InvalidConfigValueException {

		node.getClientCore().getAlerts().register(alert);

		enable(wasEnabledOnStartup);

		// Fetch seednodes to the nodeDir.
		if (updateSeednodes) {

			SimplePuller seedrefsGetter = new SimplePuller(getSeednodesURI(),
					NodeFile.Seednodes);
			seedrefsGetter.start(
					RequestStarter.IMMEDIATE_SPLITFILE_PRIORITY_CLASS,
					MAX_SEEDNODES_LENGTH);
		}

		// Fetch installers and IP-to-country files to the runDir.
		if (updateInstallers) {
			SimplePuller installerGetter = new SimplePuller(
					getInstallerNonWindowsURI(), NodeFile.InstallerNonWindows);
			SimplePuller wininstallerGetter = new SimplePuller(
					getInstallerWindowsURI(), NodeFile.InstallerWindows);

			installerGetter.start(RequestStarter.UPDATE_PRIORITY_CLASS,
					MAX_JAVA_INSTALLER_LENGTH);
			wininstallerGetter.start(RequestStarter.UPDATE_PRIORITY_CLASS,
					MAX_WINDOWS_INSTALLER_LENGTH);

		}

		if (updateIPToCountry) {
			SimplePuller ip4Getter = new SimplePuller(getIPv4ToCountryURI(),
					NodeFile.IPv4ToCountry);
			ip4Getter.start(RequestStarter.UPDATE_PRIORITY_CLASS,
					MAX_IP_TO_COUNTRY_LENGTH);
		}

	}

	void broadcastUOMAnnouncesOld() {
		boolean mainJarAvailable = transitionMainJarFetcher == null ? false
				: transitionMainJarFetcher.fetched();
		Message msg;
		if(!mainJarAvailable) return;
		synchronized (broadcastUOMAnnouncesSync) {
			if(broadcastUOMAnnouncesOld && !hasBeenBlown) return;
			broadcastUOMAnnouncesOld = true;
			msg = getOldUOMAnnouncement();
		}
		node.getPeers().localBroadcast(msg, true, true, ctr, 0, TRANSITION_VERSION-1);
	}

	void broadcastUOMAnnouncesNew() {
		if(logMINOR) Logger.minor(this, "Broadcast UOM announcements (new)");
		long size = canAnnounceUOMNew();
		Message msg;
		if(size <= 0 && !hasBeenBlown) return;
		synchronized (broadcastUOMAnnouncesSync) {
			if(broadcastUOMAnnouncesNew && !hasBeenBlown) return;
			broadcastUOMAnnouncesNew = true;
			msg = getNewUOMAnnouncement(size);
		}
		if(logMINOR) Logger.minor(this, "Broadcasting UOM announcements (new)");
		node.getPeers().localBroadcast(msg, true, true, ctr, TRANSITION_VERSION, Integer.MAX_VALUE);
	}

	/** Return the length of the data fetched for the current version, or -1. */
	private long canAnnounceUOMNew() {
		Bucket data;
		synchronized(this) {
			if(hasNewMainJar && armed) {
				if(logMINOR) Logger.minor(this, "Will update soon, not offering UOM.");
				return -1;
			}
			if(fetchedMainJarVersion <= 0) {
				if(logMINOR) Logger.minor(this, "Not fetched yet");
				return -1;
			} else if(fetchedMainJarVersion != Version.buildNumber()) {
				// Don't announce UOM unless we've successfully started the jar.
				if(logMINOR) Logger.minor(this, "Downloaded a different version than the one we are running, not offering UOM.");
				return -1;
			}
			data = fetchedMainJarData;
		}
		if(logMINOR) Logger.minor(this, "Got data for UOM: "+data+" size "+data.size());
		return data.size();
	}

	private Message getOldUOMAnnouncement() {
		boolean mainJarAvailable = transitionMainJarFetcher == null ? false
				: transitionMainJarFetcher.fetched();
        return DMT.createUOMAnnouncement(previousMainJarUSK.toString(), revocationURI
                .toString(), revocationChecker.hasBlown(),
                mainJarAvailable ? TRANSITION_VERSION : -1,
                revocationChecker.lastSucceededDelta(), revocationChecker
                .getRevocationDNFCounter(), revocationChecker
                .getBlobSize(),
                mainJarAvailable ? transitionMainJarFetcher.getBlobSize() : -1,
                (int) node.getNodeStats().getNodeAveragePingTime(),
                (int) node.getNodeStats().getBwlimitDelayTime());
	}

	private Message getNewUOMAnnouncement(long blobSize) {
		int fetchedVersion = blobSize <= 0 ? -1 : Version.buildNumber();
		if(blobSize <= 0) fetchedVersion = -1;
		return DMT.createUOMAnnouncement(updateURI.toString(), revocationURI
				.toString(), revocationChecker.hasBlown(), fetchedVersion,
				revocationChecker.lastSucceededDelta(), revocationChecker
						.getRevocationDNFCounter(), revocationChecker
						.getBlobSize(),
				blobSize,
				(int) node.getNodeStats().getNodeAveragePingTime(),
				(int) node.getNodeStats().getBwlimitDelayTime());
	}

	public void maybeSendUOMAnnounce(PeerNode peer) {
		boolean sendOld, sendNew;
		synchronized (broadcastUOMAnnouncesSync) {
			if (!(broadcastUOMAnnouncesOld || broadcastUOMAnnouncesNew)) {
				if (logMINOR)
					Logger.minor(this,
							"Not sending UOM (any) on connect: Nothing worth announcing yet");
				return; // nothing worth announcing yet
			}
			sendOld = broadcastUOMAnnouncesOld;
			sendNew = broadcastUOMAnnouncesNew;
		}
		if (hasBeenBlown && !revocationChecker.hasBlown()) {
			if (logMINOR)
				Logger.minor(this,
						"Not sending UOM (any) on connect: Local problem causing blown key");
			// Local problem, don't broadcast.
			return;
		}
		long size = canAnnounceUOMNew();
		try {
		    if(peer.getVersionNumber() < TRANSITION_VERSION) {
		        if (sendOld || hasBeenBlown)
		            peer.sendAsync(getOldUOMAnnouncement(), null, ctr);
		    } else {
		        if (sendNew || hasBeenBlown)
		            peer.sendAsync(getNewUOMAnnouncement(size), null, ctr);
		    }
		} catch (NotConnectedException e) {
			// Sad, but ignore it
		}
	}

	/**
	 * Is auto-update enabled?
	 */
	public synchronized boolean isEnabled() {
		return (mainUpdater != null);
	}

	/**
	 * Enable or disable auto-update.
	 *
	 * @param enable
	 *            Whether auto-update should be enabled.
	 * @throws InvalidConfigValueException
	 *             If enable=true and we are not running under the wrapper.
	 */
	void enable(boolean enable) throws InvalidConfigValueException {
		// FIXME 194eb7bb6f295e52d18378d805bd315c95030b24 is doubtful and incomplete.
		// if(!node.isUsingWrapper()){
		// Logger.normal(this,
		// "Don't try to start the updater as we are not running under the wrapper.");
		// return;
		// }
		NodeUpdater main = null;
		Map<String, PluginJarUpdater> oldPluginUpdaters = null;
		// We need to run the revocation checker even if auto-update is
		// disabled.
		// Two reasons:
		// 1. For the benefit of other nodes, and because even if auto-update is
		// off, it's something the user should probably know about.
		// 2. When the key is blown, we turn off auto-update!!!!
		revocationChecker.start(false);
		synchronized (this) {
			boolean enabled = (mainUpdater != null);
			if (enabled == enable)
				return;
			if (!enable) {
				// Kill it
				mainUpdater.preKill();
				main = mainUpdater;
				mainUpdater = null;
				oldPluginUpdaters = pluginUpdaters;
				pluginUpdaters = null;
				disabledNotBlown = false;
			} else {
				// if((!WrapperManager.isControlledByNativeWrapper()) ||
				// (NodeStarter.extBuildNumber == -1)) {
				// Logger.error(this,
				// "Cannot update because not running under wrapper");
				// throw new
				// InvalidConfigValueException(l10n("noUpdateWithoutWrapper"));
				// }
				// Start it
				mainUpdater = new MainJarUpdater(this, updateURI,
						Version.buildNumber(), -1, Integer.MAX_VALUE,
						"main-jar-");
				pluginUpdaters = new HashMap<String, PluginJarUpdater>();
			}
		}
		if (!enable) {
			if (main != null)
				main.kill();
			stopPluginUpdaters(oldPluginUpdaters);
			transitionMainJarFetcher.stop();
		} else {
			// FIXME copy it, dodgy locking.
			try {
				// Must be run before starting everything else as it cleans up tempfiles too.
				mainUpdater.cleanupDependencies();
			} catch (Throwable t) {
				// Don't let it block startup, but be very loud!
				Logger.error(this, "Caught "+t+" setting up Update Over Mandatory", t);
				System.err.println("Updater error: "+t);
				t.printStackTrace();
			}
			mainUpdater.start();
			startPluginUpdaters();
			transitionMainJarFetcher.start();
		}
	}

	private void startPluginUpdaters() {
		for(OfficialPluginDescription plugin : node.getPluginManager().getOfficialPlugins()) {
			startPluginUpdater(plugin.name);
		}
	}

	/**
	 * @param plugName
	 *            The filename for loading/config purposes for an official
	 *            plugin. E.g. "Library" (no .jar)
	 */
	public void startPluginUpdater(String plugName) {
		if (logMINOR)
			Logger.minor(this, "Starting plugin updater for " + plugName);
		OfficialPluginDescription plugin = node.getPluginManager().getOfficialPlugin(plugName);
		if (plugin != null)
			startPluginUpdater(plugin);
		else
		// Most likely not an official plugin
		if (logMINOR)
			Logger.minor(this, "No such plugin " + plugName
					+ " in startPluginUpdater()");
	}

	void startPluginUpdater(OfficialPluginDescription plugin) {
		String name = plugin.name;
		// @see https://emu.freenetproject.org/pipermail/devl/2015-November/038581.html
		long minVer = (plugin.essential ? plugin.minimumVersion : plugin.recommendedVersion);
		// But it might already be past that ...
		PluginInfoWrapper info = node.getPluginManager().getPluginInfo(name);
		if (info == null) {
			if (!(node.getPluginManager().isPluginLoadedOrLoadingOrWantLoad(name))) {
				if (logMINOR)
					Logger.minor(this, "Plugin not loaded");
				return;
			}
		}
		if (info != null)
			minVer = Math.max(minVer, info.getPluginLongVersion());
		FreenetURI uri = updateURI.setDocName(name).setSuggestedEdition(minVer);
		PluginJarUpdater updater = new PluginJarUpdater(this, uri,
				(int) minVer, -1, (plugin.essential ? (int)minVer : Integer.MAX_VALUE)
				, name + "-", name, node.getPluginManager(), autoDeployPluginsOnRestart);
		synchronized (this) {
			if (pluginUpdaters == null) {
				if (logMINOR)
					Logger.minor(this, "Updating not enabled");
				return; // Not enabled
			}
			if (pluginUpdaters.containsKey(name)) {
				if (logMINOR)
					Logger.minor(this, "Already in updaters list");
				return; // Already started
			}
			pluginUpdaters.put(name, updater);
		}
		updater.start();
		System.out.println("Started plugin update fetcher for " + name);
	}

	public void stopPluginUpdater(String plugName) {
		OfficialPluginDescription plugin = node.getPluginManager().getOfficialPlugin(plugName);
		if (plugin == null)
			return; // Not an official plugin
		PluginJarUpdater updater = null;
		synchronized (this) {
			if (pluginUpdaters == null) {
				if (logMINOR)
					Logger.minor(this, "Updating not enabled");
				return; // Not enabled
			}
			updater = pluginUpdaters.remove(plugName);
		}
		if (updater != null)
			updater.kill();
	}

	private void stopPluginUpdaters(
			Map<String, PluginJarUpdater> oldPluginUpdaters) {
		for (PluginJarUpdater u : oldPluginUpdaters.values()) {
			u.kill();
		}
	}

	/**
	 * Create a NodeUpdateManager. Called by node constructor.
	 *
	 * @param node
	 *            The node object.
	 * @param config
	 *            The global config object. Options will be added to a subconfig
	 *            called node.updater.
	 * @return A new NodeUpdateManager
	 * @throws InvalidConfigValueException
	 *             If there is an error in the config.
	 */
	public static NodeUpdateManager maybeCreate(Node node, Config config)
			throws InvalidConfigValueException {
		return new NodeUpdateManager(node, config);
	}

	/**
	 * Get the URI for freenet.jar.
	 */
	public synchronized FreenetURI getURI() {
		return updateURI;
	}

	/**
	 * @return URI for the user-facing changelog.
	 */
	public synchronized FreenetURI getChangelogURI() {
		return updateURI.setDocName("changelog");
	}

	public synchronized FreenetURI getDeveloperChangelogURI() {
		return updateURI.setDocName("fullchangelog");
	}

	/**
	 * Add links to the changelog for the given version to the given node.
	 * @param version USK edition to point to
	 * @param node to add links to
	 */
	public synchronized void addChangelogLinks(long version, HTMLNode node) {
		String changelogUri = getChangelogURI().setSuggestedEdition(version).sskForUSK().toASCIIString();
		String developerDetailsUri = getDeveloperChangelogURI().setSuggestedEdition(version).sskForUSK().toASCIIString();
		node.addChild("a", "href", '/' + changelogUri + "?type=text/plain",
			NodeL10n.getBase().getString("UpdatedVersionAvailableUserAlert.changelog"));
		node.addChild("br");
		node.addChild("a", "href", '/' + developerDetailsUri + "?type=text/plain",
			NodeL10n.getBase().getString("UpdatedVersionAvailableUserAlert.devchangelog"));
	}

	/**
	 * Set the URfrenet.jar should be updated from.
	 *
	 * @param uri
	 *            The URI to set.
	 */
	public void setURI(FreenetURI uri) {
		// FIXME plugins!!
		NodeUpdater updater;
		Map<String, PluginJarUpdater> oldPluginUpdaters = null;
		synchronized (this) {
			if (updateURI.equals(uri))
				return;
			updateURI = uri;
			updateURI = updateURI.setSuggestedEdition(Version.buildNumber());
			updater = mainUpdater;
			oldPluginUpdaters = pluginUpdaters;
			pluginUpdaters = new HashMap<String, PluginJarUpdater>();
			if (updater == null)
				return;
		}
		updater.onChangeURI(uri);
		stopPluginUpdaters(oldPluginUpdaters);
		startPluginUpdaters();
	}

	/** @return The revocation URI. */
	public synchronized FreenetURI getRevocationURI() {
		return revocationURI;
	}

	/**
	 * Set the revocation URI.
	 *
	 * @param uri
	 *            The new revocation URI.
	 */
	public void setRevocationURI(FreenetURI uri) {
		synchronized (this) {
			if (revocationURI.equals(uri))
				return;
			this.revocationURI = uri;
		}
		revocationChecker.onChangeRevocationURI();
	}

	/**
	 * @return Is auto-update currently enabled?
	 */
	public boolean isAutoUpdateAllowed() {
		return isAutoUpdateAllowed;
	}

	/**
	 * Enable or disable auto-update.
	 *
	 * @param val
	 *            If true, enable auto-update (and immediately update if an
	 *            update is ready). If false, disable it.
	 */
	public void setAutoUpdateAllowed(boolean val) {
		synchronized (this) {
			if (val == isAutoUpdateAllowed)
				return;
			isAutoUpdateAllowed = val;
			if (val) {
				if (!isReadyToDeployUpdate(false))
					return;
			} else
				return;
		}
		deployOffThread(0, false);
	}

	private static final long WAIT_FOR_SECOND_FETCH_TO_COMPLETE = MINUTES.toMillis(4);
	private static final long RECENT_REVOCATION_INTERVAL = MINUTES.toMillis(2);
	/**
	 * After 5 minutes, deploy the update even if we haven't got 3 DNFs on the
	 * revocation key yet. Reason: we want to be able to deploy UOM updates on
	 * nodes with all TOO NEW or leaf nodes whose peers are overloaded/broken.
	 * Note that with UOM, revocation certs are automatically propagated node to
	 * node, so this should be *relatively* safe. Any better ideas, tell us.
	 */
	private static final long REVOCATION_FETCH_TIMEOUT = MINUTES.toMillis(5);

	/**
	 * Does the updater have an update ready to deploy? May be called
	 * synchronized(this).
	 * @param ignoreRevocation If true, return whether we will deploy when the revocation check
	 * finishes. If false, return whether we can deploy now, and if not, deploy after a delay with
	 * deployOffThread().
	 */
	private boolean isReadyToDeployUpdate(boolean ignoreRevocation) {
		long now = System.currentTimeMillis();
		int waitForNextJar = -1;
		synchronized (this) {
			if (mainUpdater == null)
				return false;
			if (!(hasNewMainJar)) {
				return false; // no jar
			}
			if (hasBeenBlown)
				return false; // Duh
			if (peersSayBlown) {
				if(logMINOR) Logger.minor(this, "Not deploying, peers say blown");
				return false;
			}
			// Don't immediately deploy if still fetching
			if (startedFetchingNextMainJar > 0) {
				waitForNextJar = (int) (startedFetchingNextMainJar
						+ WAIT_FOR_SECOND_FETCH_TO_COMPLETE - now);
				if (waitForNextJar > 0) {
					if (logMINOR)
						Logger.minor(this, "Not ready: Still fetching");
					// Wait for running fetch to complete
				}
			}

			// Check dependencies.
			if (this.latestMainJarDependencies == null) {
				if (logMINOR)
					Logger.minor(this, "Dependencies not available");
				return false;
			}
			if (this.fetchedMainJarVersion != this.dependenciesValidForBuild) {
				if (logMINOR)
					Logger.minor(this,
							"Not deploying because dependencies are older version "
									+ dependenciesValidForBuild
									+ " - new version " + fetchedMainJarVersion
									+ " may not start");
				return false;
			}

			// Check revocation.
			if (waitForNextJar <= 0) {
				if (!ignoreRevocation) {
					if (now - revocationChecker.lastSucceeded() < RECENT_REVOCATION_INTERVAL) {
						if(logMINOR) Logger.minor(this, "Ready to deploy (revocation checker succeeded recently)");
						return true;
					}
					if (gotJarTime > 0
							&& now - gotJarTime >= REVOCATION_FETCH_TIMEOUT) {
						if(logMINOR) Logger.minor(this, "Ready to deploy (got jar before timeout)");
						return true;
					}
				}
			}
		}
		if (logMINOR)
			Logger.minor(this, "Still here in isReadyToDeployUpdate");
		// Apparently everything is ready except the revocation fetch. So start
		// it.
		revocationChecker.start(true);
		if (ignoreRevocation) {
			if (logMINOR)
				Logger.minor(this, "Returning true because of ignoreRevocation");
			return true;
		}
		long waitTime = Math.max(REVOCATION_FETCH_TIMEOUT, waitForNextJar);
		if(logMINOR) Logger.minor(this, "Will deploy in "+waitTime+"ms");
		deployOffThread(waitTime, false);
		return false;
	}

	/** Check whether there is an update to deploy. If there is, do it. */
	private void deployUpdate() {
		boolean started = false;
		boolean success = false;
		try {
			MainJarDependencies deps;
			synchronized (this) {
				if (disabledThisSession) {
					String msg = "Not deploying update because disabled for this session (bad java version??)";
					Logger.error(this, msg);
					System.err.println(msg);
					return;
				}
				if (hasBeenBlown) {
					String msg = "Trying to update but key has been blown! Not updating, message was "
							+ revocationMessage;
					Logger.error(this, msg);
					System.err.println(msg);
					return;
				}
				if (peersSayBlown) {
					String msg = "Trying to update but at least one peer says the key has been blown! Not updating.";
					Logger.error(this, msg);
					System.err.println(msg);
					return;

				}
				if (!isEnabled()) {
					if (logMINOR)
						Logger.minor(this, "Not enabled");
					return;
				}
				if (!(isAutoUpdateAllowed || armed)) {
					if (logMINOR)
						Logger.minor(this, "Not armed");
					return;
				}
				if (!isReadyToDeployUpdate(false)) {
					if (logMINOR)
						Logger.minor(this, "Not ready to deploy update");
					return;
				}
				if (isDeployingUpdate) {
					if (logMINOR)
						Logger.minor(this, "Already deploying update");
					return;
				}
				started = true;
				isDeployingUpdate = true;
				deps = latestMainJarDependencies;
			}

			synchronized(deployLock()) {
			    success = innerDeployUpdate(deps);
			    if(success) waitForever();
			}
			// isDeployingUpdate remains true as we are about to restart.
		} catch (Throwable t) {
			Logger.error(this, "DEPLOYING UPDATE FAILED: "+t, t);
			System.err.println("UPDATE FAILED: CAUGHT "+t);
			System.err.println("YOUR NODE DID NOT UPDATE. THIS IS PROBABLY A BUG OR SERIOUS PROBLEM SUCH AS OUT OF MEMORY.");
			System.err.println("Cause of the problem: "+t);
			t.printStackTrace();
			failUpdate(t.getMessage());
			String error = l10n("updateFailedInternalError", "reason", t.getMessage());
			node.getClientCore().getAlerts().register(new SimpleUserAlert(false,
					error, error, error, UserAlert.CRITICAL_ERROR));
		} finally {
			if(started && !success) {
				Bucket toFree = null;
				synchronized (this) {
					isDeployingUpdate = false;
					if (maybeNextMainJarVersion > fetchedMainJarVersion) {
						// A newer version has been fetched in the meantime.
						toFree = fetchedMainJarData;
						fetchedMainJarVersion = maybeNextMainJarVersion;
						fetchedMainJarData = maybeNextMainJarData;
						maybeNextMainJarVersion = -1;
						maybeNextMainJarData = null;
					}
				}
				if (toFree != null)
					toFree.free();
			}
		}
	}

	/** Use this lock when deploying an update of any kind which will require us to restart. If the
	 * update succeeds, you should call waitForever() if you don't immediately exit. There could be
	 * rather nasty race conditions if we deploy two updates at once.
	 * @return A mutex for serialising update deployments. */
	static final Object deployLock() {
	    return deployLock;
	}

	/** Does not return. Should be called, inside the deployLock(), if you are in a situation
	 * where you've deployed an update but the exit hasn't actually happened yet. */
	static void waitForever() {
	    while(true) {
	        System.err.println("Waiting for shutdown after deployed update...");
	        try {
                Thread.sleep(60*1000);
            } catch (InterruptedException e) {
                // Ignore.
            }
	    }
	}

	/**
	 * Deploy the update. Inner method. Doesn't check anything, just does it.
	 */
	private boolean innerDeployUpdate(MainJarDependencies deps) {
		System.err.println("Deploying update "+deps.build+" with "+deps.dependencies.size()+" dependencies...");
		// Write the jars, config etc.
		// Then restart

		UpdateDeployContext ctx;
		try {
			ctx = new UpdateDeployContext(deps);
		} catch (UpdaterParserException e) {
			failUpdate("Could not determine which jars are in use: "
					+ e.getMessage());
			return false;
		}

		if (writeJars(ctx, deps)) {
			restart(ctx);
			return true;
		} else {
			if (logMINOR)
				Logger.minor(this, "Did not write jars");
			return false;
		}
	}

	/**
	 * Write the updated jars, if necessary rewrite the wrapper.conf.
	 *
	 * @return True if this part of the update succeeded.
	 */
	private boolean writeJars(UpdateDeployContext ctx, MainJarDependencies deps) {
		/**
		 * What do we want to do here? 1. If we have a new main jar: - If on
		 * Windows, write it to a new jar file, update the wrapper.conf to point
		 * to it. - Otherwise, write to a new jar file, then move the new jar
		 * file over the old jar file. 2. If the dependencies have changed, we
		 * need to update wrapper.conf.
		 */

		boolean writtenNewJar = false;

		boolean tryEasyWay = File.pathSeparatorChar == ':'
				&& (!deps.mustRewriteWrapperConf);

		if (hasNewMainJar) {
			File mainJar = ctx.getMainJar();
			File newMainJar = ctx.getNewMainJar();
			File backupJar = ctx.getBackupJar();
			try {
				if (writeJar(mainJar, newMainJar, backupJar, mainUpdater, "main",
						tryEasyWay))
					writtenNewJar = true;
			} catch (UpdateFailedException e) {
				failUpdate(e.getMessage());
				return false;
			}
		}

		// Dependencies have been written for us already.
		// But we may need to modify wrapper.conf.

		if (!(writtenNewJar || deps.mustRewriteWrapperConf))
			return true;
		try {
			ctx.rewriteWrapperConf(writtenNewJar);
		} catch (IOException e) {
			failUpdate("Cannot rewrite wrapper.conf: " + e);
			return false;
		} catch (UpdateCatastropheException e) {
			failUpdate(e.getMessage());
			node.getClientCore().getAlerts().register(new SimpleUserAlert(false,
					l10n("updateCatastropheTitle"), e.getMessage(),
					l10n("updateCatastropheTitle"), UserAlert.CRITICAL_ERROR));
			return false;
		} catch (UpdaterParserException e) {
			node.getClientCore().getAlerts().register(new SimpleUserAlert(false,
					l10n("updateFailedTitle"), e.getMessage(), l10n(
							"updateFailedShort", "reason", e.getMessage()),
					UserAlert.CRITICAL_ERROR));
			return false;
		}

		return true;
	}

	/**
	 * Write a jar. Returns true if the caller needs to rewrite the config,
	 * false if he doesn't, or throws if it fails.
	 *
	 * @param mainJar
	 *            The location of the current jar file.
	 * @param newMainJar
	 *            The location of the new jar file.
	 * @param backupMainJar
	 *            On Windows, we alternate between freenet.jar and freenet.jar.new, so we do not
	 *            need to write a backup - the user can rename between these two. On Unix, we
	 *            copy to freenet.jar.bak before updating, in case something horrible happens.
	 * @param mainUpdater
	 *            The NodeUpdater for the file in question, so we can ask it to
	 *            write the file.
	 * @param name
	 *            The name of the jar for logging.
	 * @param tryEasyWay
	 *            If true, attempt to rename the new file directly over the old
	 *            one. This avoids the need to rewrite the wrapper config file.
	 * @return True if the caller needs to rewrite the config, false if he
	 *         doesn't (because easy way worked).
	 * @throws UpdateFailedException
	 *             If something breaks.
	 */
	private boolean writeJar(File mainJar, File newMainJar, File backupMainJar,
			NodeUpdater mainUpdater, String name, boolean tryEasyWay)
			throws UpdateFailedException {
		boolean writtenToTempFile = false;
		try {
			if (newMainJar.exists()) {
				if (!newMainJar.delete()) {
					if (newMainJar.exists()) {
						System.err
								.println("Cannot write to preferred new jar location "
										+ newMainJar);
						if (tryEasyWay) {
							try {
								newMainJar = File.createTempFile("freenet",
										".jar", mainJar.getParentFile());
							} catch (IOException e) {
								throw new UpdateFailedException(
										"Cannot write to any other location either - disk full? "
												+ e);
							}
							// Try writing to it
							try {
								writeJarTo(newMainJar);
								writtenToTempFile = true;
							} catch (IOException e) {
								newMainJar.delete();
								throw new UpdateFailedException(
										"Cannot write new jar - disk full? "
												+ e);
							}
						} else {
							// Try writing it to the new one even though we
							// can't delete it.
							writeJarTo(newMainJar);
						}
					} else {
						writeJarTo(newMainJar);
					}
				} else {
					if (logMINOR)
						Logger.minor(NodeUpdateManager.class,
								"Deleted old jar " + newMainJar);
					writeJarTo(newMainJar);
				}
			} else {
				writeJarTo(newMainJar);
			}
			System.out.println("Written new main jar to "+newMainJar);
		} catch (IOException e) {
			throw new UpdateFailedException("Cannot update: Cannot write to "
					+ (tryEasyWay ? " temp file " : "new jar ") + newMainJar);
		}

		if (tryEasyWay) {
			// Do it the easy way. Just rewrite the main jar.
			backupMainJar.delete();
			if(FileUtil.copyFile(mainJar, backupMainJar))
				System.err.println("Written backup of current main jar to "+backupMainJar+" (if freenet fails to start up try renaming "+backupMainJar+" over "+mainJar);
			if (!newMainJar.renameTo(mainJar)) {
				Logger.error(NodeUpdateManager.class,
						"Cannot rename temp file " + newMainJar
								+ " over original jar " + mainJar);
				if (writtenToTempFile) {
					// Fail the update - otherwise we will leak disk space
					newMainJar.delete();
					throw new UpdateFailedException(
							"Cannot write to preferred new jar location and cannot rename temp file over old jar, update failed");
				}
				// Try the hard way
			} else {
				System.err.println("Completed writing new Freenet jar to "+mainJar+".");
				return false;
			}
		}
		System.err.println("Rewriting wrapper.conf to point to "+newMainJar+" rather than "+mainJar+" (if Freenet fails to start after the update you could try changing wrapper.conf to use the old jar)");
		return true;
	}

	public void writeJarTo(File fNew) throws IOException {
		if (!fNew.delete() && fNew.exists()) {
			System.err.println("Can't delete " + fNew + "!");
		}

		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(fNew);

			BucketTools.copyTo(this.fetchedMainJarData, fos, -1);

			fos.flush();
		} finally {
			Closer.close(fos);
		}
	}

	@SuppressWarnings("serial")
	private static class UpdateFailedException extends Exception {

		public UpdateFailedException(String message) {
			super(message);
		}

	}

	/** Restart the node. Does not return. */
	private void restart(UpdateDeployContext ctx) {
		if (logMINOR)
			Logger.minor(this, "Restarting...");
		node.getNodeStarter().restart();
		try {
			Thread.sleep(MINUTES.toMillis(5));
		} catch (InterruptedException e) {
			// Break
		} // in case it's still restarting
		System.err
				.println("Failed to restart. Exiting, please restart the node.");
		System.exit(NodeInitException.EXIT_RESTART_FAILED);
	}

	private void failUpdate(String reason) {
		Logger.error(this, "Update failed: " + reason);
		System.err.println("Update failed: " + reason);
		this.killUpdateAlerts();
		node.getClientCore().getAlerts().register(new SimpleUserAlert(true,
				l10n("updateFailedTitle"), l10n("updateFailed", "reason",
						reason), l10n("updateFailedShort", "reason", reason),
				UserAlert.CRITICAL_ERROR));
	}

	private String l10n(String key) {
		return NodeL10n.getBase().getString("NodeUpdateManager." + key);
	}

	private String l10n(String key, String pattern, String value) {
		return NodeL10n.getBase().getString("NodeUpdateManager." + key,
				pattern, value);
	}

	/**
	 * Called when a new jar has been downloaded. The caller should process the
	 * dependencies *AFTER* this method has completed, and then call
	 * onDependenciesReady().
	 *
	 * @param fetched
	 *            The build number we have fetched.
	 * @param result
	 *            The actual data.
	 */
	void onDownloadedNewJar(Bucket result, int fetched, File savedBlob) {
		Bucket delete1 = null;
		Bucket delete2 = null;
		synchronized (this) {
			if (fetched > Version.buildNumber()) {
				hasNewMainJar = true;
				startedFetchingNextMainJar = -1;
				gotJarTime = System.currentTimeMillis();
				if (logMINOR)
					Logger.minor(this, "Got main jar: " + fetched);
			}
			if (!isDeployingUpdate) {
				delete1 = fetchedMainJarData;
				fetchedMainJarVersion = fetched;
				fetchedMainJarData = result;
				if(fetched == Version.buildNumber()) {
					if(savedBlob != null)
						currentVersionBlobFile = savedBlob;
					else
						Logger.error(this, "No blob file for latest version?!", new Exception("error"));
				}
			} else {
				delete2 = maybeNextMainJarData;
				maybeNextMainJarVersion = fetched;
				maybeNextMainJarData = result;
				System.out
						.println("Already deploying update, not using new main jar #"
								+ fetched);
			}
		}
		if (delete1 != null)
			delete1.free();
		if (delete2 != null)
			delete2.free();
		// We cannot deploy yet, we must wait for the dependencies check.
	}

	/**
	 * Called when the NodeUpdater starts to fetch a new version of the jar.
	 */
	void onStartFetching() {
		long now = System.currentTimeMillis();
		synchronized (this) {
			startedFetchingNextMainJar = now;
		}
	}

	private boolean disabledNotBlown;

	/**
	 * @param msg
	 * @param disabledNotBlown
	 *            If true, the auto-updating system is broken, and should be
	 *            disabled, but the problem *could* be local e.g. out of disk
	 *            space and a node sends us a revocation certificate.
	 */
	public void blow(String msg, boolean disabledNotBlown) {
		NodeUpdater main;
		synchronized (this) {
			if (hasBeenBlown) {
				if (this.disabledNotBlown && !disabledNotBlown)
					disabledNotBlown = true;
				Logger.error(this,
						"The key has ALREADY been marked as blown! Message was "
								+ revocationMessage + " new message " + msg);
				return;
			} else {
				this.revocationMessage = msg;
				this.hasBeenBlown = true;
				this.disabledNotBlown = disabledNotBlown;
				// We must get to the lower part, and show the user the message
				try {
					if (disabledNotBlown) {
						System.err
								.println("THE AUTO-UPDATING SYSTEM HAS BEEN DISABLED!");
						System.err
								.println("We do not know whether this is a local problem or the auto-update system has in fact been compromised. What we do know:\n"
										+ revocationMessage);
					} else {
						System.err
								.println("THE AUTO-UPDATING SYSTEM HAS BEEN COMPROMISED!");
						System.err
								.println("The auto-updating system revocation key has been inserted. It says: "
										+ revocationMessage);
					}
				} catch (Throwable t) {
					try {
						Logger.error(this, "Caught " + t, t);
					} catch (Throwable t1) {
					}
				}
			}
			main = mainUpdater;
			if (main != null)
				main.preKill();
			mainUpdater = null;
		}
		if (main != null)
			main.kill();
		if (revocationAlert == null) {
			revocationAlert = new RevocationKeyFoundUserAlert(msg,
					disabledNotBlown);
			node.getClientCore().getAlerts().register(revocationAlert);
			// we don't need to advertize updates : we are not going to do them
			killUpdateAlerts();
		}
		uom.killAlert();
		broadcastUOMAnnouncesOld();
		broadcastUOMAnnouncesNew();
	}

	/**
	 * Kill all UserAlerts asking the user whether he wants to update.
	 */
	private void killUpdateAlerts() {
		node.getClientCore().getAlerts().unregister(alert);
	}

	/** Called when the RevocationChecker has got 3 DNFs on the revocation key */
	public void noRevocationFound() {
		deployUpdate(); // May have been waiting for the revocation.
		deployPluginUpdates();
		// If we're still here, we didn't update.
		broadcastUOMAnnouncesNew();
		node.getTicker().queueTimedJob(new Runnable() {
			@Override
			public void run() {
				revocationChecker.start(false);
			}
		}, node.getRandom().nextInt((int) DAYS.toMillis(1)));
	}

	private void deployPluginUpdates() {
		PluginJarUpdater[] updaters = null;
		synchronized (this) {
			if (this.pluginUpdaters != null)
				updaters = pluginUpdaters.values().toArray(
						new PluginJarUpdater[pluginUpdaters.size()]);
		}
		boolean restartRevocationFetcher = false;
		if (updaters != null) {
			for (PluginJarUpdater u : updaters) {
				if (u.onNoRevocation())
					restartRevocationFetcher = true;
			}
		}
		if (restartRevocationFetcher)
			revocationChecker.start(true, true);
	}

	public void arm() {
		armed = true;
		OpennetManager om = node.getOpennet();
		if (om != null) {
			if (om.waitingForUpdater()) {
				synchronized (this) {
					// Reannounce and count it from now.
					if (gotJarTime > 0)
						gotJarTime = System.currentTimeMillis();
				}
				om.reannounce();
			}
		}
		deployOffThread(0, false);
	}

	void deployOffThread(long delay, final boolean announce) {
		node.getTicker().queueTimedJob(new Runnable() {
			@Override
			public void run() {
				if(announce)
					maybeBroadcastUOMAnnouncesNew();
				if (logMINOR)
					Logger.minor(this, "Running deployOffThread");
				deployUpdate();
				if (logMINOR)
					Logger.minor(this, "Run deployOffThread");
			}
		}, delay);
	}

	protected void maybeBroadcastUOMAnnouncesNew() {
		if(logMINOR) Logger.minor(this, "Maybe broadcast UOM announces new");
		synchronized(NodeUpdateManager.this) {
			if(hasBeenBlown) return;
			if(peersSayBlown) return;
		}
		if(logMINOR) Logger.minor(this, "Maybe broadcast UOM announces new (2)");
		// If the node has no peers, noRevocationFound will never be called.
		broadcastUOMAnnouncesNew();
	}

	/**
	 * Has the private key been revoked?
	 */
	public boolean isBlown() {
		return hasBeenBlown;
	}

	public boolean hasNewMainJar() {
		return hasNewMainJar;
	}

	/**
	 * What version has been fetched?
	 *
	 * This includes jar's fetched via UOM, because the UOM code feeds its
	 * results through the mainUpdater.
	 */
	public int newMainJarVersion() {
		if (mainUpdater == null)
			return -1;
		return mainUpdater.getFetchedVersion();
	}

	public boolean fetchingNewMainJar() {
		return (mainUpdater != null && mainUpdater.isFetching());
	}

	public int fetchingNewMainJarVersion() {
		if (mainUpdater == null)
			return -1;
		return mainUpdater.fetchingVersion();
	}

	public boolean inFinalCheck() {
		return isReadyToDeployUpdate(true) && !isReadyToDeployUpdate(false);
	}

	public int getRevocationDNFCounter() {
		return revocationChecker.getRevocationDNFCounter();
	}

	/**
	 * What version is the node currently running?
	 */
	public int getMainVersion() {
		return Version.buildNumber();
	}

	public int getExtVersion() {
		return NodeStarter.extBuildNumber;
	}

	public boolean isArmed() {
		return armed || isAutoUpdateAllowed;
	}

	/**
	 * Is the node able to update as soon as the revocation fetch has been
	 * completed?
	 */
	public boolean canUpdateNow() {
		return isReadyToDeployUpdate(true);
	}

	/**
	 * Is the node able to update *immediately*? (i.e. not only is it ready in
	 * every other sense, but also a revocation fetch has completed recently
	 * enough not to need another one)
	 */
	public boolean canUpdateImmediately() {
		return isReadyToDeployUpdate(false);
	}

	// Config callbacks

	class UpdaterEnabledCallback extends BooleanCallback {

		@Override
		public Boolean get() {
			if (isEnabled())
				return true;
			synchronized (NodeUpdateManager.this) {
				if (disabledNotBlown)
					return true;
			}
			return false;
		}

		@Override
		public void set(Boolean val) throws InvalidConfigValueException {
			enable(val);
		}
	}

	class AutoUpdateAllowedCallback extends BooleanCallback {

		@Override
		public Boolean get() {
			return isAutoUpdateAllowed();
		}

		@Override
		public void set(Boolean val) throws InvalidConfigValueException {
			setAutoUpdateAllowed(val);
		}
	}

	class UpdateURICallback extends StringCallback {

		@Override
		public String get() {
			return getURI().toString(false, false);
		}

		@Override
		public void set(String val) throws InvalidConfigValueException {
			FreenetURI uri;
			try {
				uri = new FreenetURI(val);
			} catch (MalformedURLException e) {
				throw new InvalidConfigValueException(l10n(
						"invalidUpdateURI", "error",
						e.getLocalizedMessage()));
			}
			if(uri.hasMetaStrings())
				throw new InvalidConfigValueException(l10n("updateURIMustHaveNoMetaStrings"));
			if(!uri.isUSK())
				throw new InvalidConfigValueException(l10n("updateURIMustBeAUSK"));
			setURI(uri);
		}
	}

	public class UpdateRevocationURICallback extends StringCallback {

		@Override
		public String get() {
			return getRevocationURI().toString(false, false);
		}

		@Override
		public void set(String val) throws InvalidConfigValueException {
			FreenetURI uri;
			try {
				uri = new FreenetURI(val);
			} catch (MalformedURLException e) {
				throw new InvalidConfigValueException(l10n(
						"invalidRevocationURI", "error",
						e.getLocalizedMessage()));
			}
			setRevocationURI(uri);
		}
	}

	/**
	 * Called when a peer indicates in its UOMAnnounce that it has fetched the
	 * revocation key (or failed to do so in a way suggesting that somebody
	 * knows the key).
	 *
	 * @param source
	 *            The node which is claiming this.
	 */
	void peerClaimsKeyBlown() {
		// Note that UpdateOverMandatoryManager manages the list of peers who
		// think this.
		// All we have to do is cancel the update.

		peersSayBlown = true;
	}

	/** Called inside locks, so don't lock anything */
	public void notPeerClaimsKeyBlown() {
		peersSayBlown = false;
		node.getExecutor().execute(new Runnable() {

			@Override
			public void run() {
				if(isReadyToDeployUpdate(false))
					deployUpdate();
			}

		}, "Check for updates");
		node.getTicker().queueTimedJob(new Runnable() {

			@Override
			public void run() {
				maybeBroadcastUOMAnnouncesNew();
			}

		}, REVOCATION_FETCH_TIMEOUT);
	}

	boolean peersSayBlown() {
		return peersSayBlown;
	}

	public File getMainBlob(int version) {
		NodeUpdater updater;
		synchronized (this) {
			if (hasBeenBlown)
				return null;
			updater = mainUpdater;
			if (updater == null)
				return null;
		}
		return updater.getBlobFile(version);
	}

	public synchronized long timeRemainingOnCheck() {
		long now = System.currentTimeMillis();
		return Math.max(0, REVOCATION_FETCH_TIMEOUT - (now - gotJarTime));
	}

	/**
	 * @deprecated Use {@link #getByteCounter()} instead of accessing this directly.
	 */
	@Deprecated
	/* It’s not the field that is deprecated but accessing it directly is. */
	final ByteCounter ctr = new ByteCounter() {

		@Override
		public void receivedBytes(int x) {
			// FIXME
		}

		@Override
		public void sentBytes(int x) {
			node.getNodeStats().reportUOMBytesSent(x);
		}

		@Override
		public void sentPayload(int x) {
			// Ignore. It will be reported to sentBytes() as well.
		}

	};

	public void disableThisSession() {
		disabledThisSession = true;
	}

	protected long getStartedFetchingNextMainJarTimestamp() {
		return startedFetchingNextMainJar;
	}

	public void disconnected(PeerNode pn) {
		uom.disconnected(pn);
	}

	public void deployPlugin(String fn) throws IOException {
		PluginJarUpdater updater;
		synchronized (this) {
			if (hasBeenBlown) {
				Logger.error(this, "Not deploying update for " + fn
						+ " because revocation key has been blown!");
				return;
			}
			updater = pluginUpdaters.get(fn);
		}
		updater.writeJar();
	}

	public void deployPluginWhenReady(String fn) throws IOException {
		PluginJarUpdater updater;
		synchronized (this) {
			if (hasBeenBlown) {
				Logger.error(this, "Not deploying update for " + fn
						+ " because revocation key has been blown!");
				return;
			}
			updater = pluginUpdaters.get(fn);
		}
		boolean wasRunning = revocationChecker.start(true, true);
		updater.arm(wasRunning);
	}

	public boolean dontAllowUOM() {
		if (node.isOpennetEnabled() && node.wantAnonAuth(true)) {
			// We are a seednode.
			// Normally this means we won't send UOM.
			// However, if something breaks severely, we need an escape route.
			if (node.getUptime() > MINUTES.toMillis(5)
					&& node.getPeers().countCompatibleRealPeers() == 0)
				return false;
			return true;
		}
		return false;
	}

	public boolean fetchingFromUOM() {
		return uom.isFetchingMain();
	}

	/**
	 * Called when the dependencies have been verified and/or downloaded, and we
	 * can upgrade to the new build without dependency issues.
	 *
	 * @param deps
	 *            The dependencies object. Used to rewrite wrapper.conf if
	 *            necessary. Also contains the build number.
	 * @param binaryBlob
	 *            The binary blob for this build, including the dependencies.
	 */
	public void onDependenciesReady(MainJarDependencies deps) {
		synchronized (this) {
			this.latestMainJarDependencies = deps;
			this.dependenciesValidForBuild = deps.build;
		}
		revocationChecker.start(true);
		// Deploy immediately if the revocation checker has already reported in but we were waiting for deps.
		// Otherwise wait for the revocation checker.
		deployOffThread(0, true);
	}

	public File getTransitionMainBlob() {
		return transitionMainJarFetcher.getBlobFile();
	}

	/** Show the progress of individual dependencies if possible */
	public void renderProgress(HTMLNode alertNode) {
		MainJarUpdater m;
		synchronized (this) {
			if(this.fetchedMainJarData == null) return;
			m = mainUpdater;
			if(m == null) return;
		}
		m.renderProperties(alertNode);
	}

	public boolean brokenDependencies() {
		MainJarUpdater m;
		synchronized (this) {
			m = mainUpdater;
			if(m == null) return false;
		}
		return m.brokenDependencies();
	}

	public void onStartFetchingUOM() {
		MainJarUpdater m;
		synchronized (this) {
			m = mainUpdater;
			if(m == null) return;
		}
		m.onStartFetchingUOM();
	}

	public synchronized File getCurrentVersionBlobFile() {
		if(hasNewMainJar) return null;
		if(isDeployingUpdate) return null;
		if(fetchedMainJarVersion != Version.buildNumber()) return null;
		return currentVersionBlobFile;
	}

	MainJarUpdater getMainUpdater() {
		return mainUpdater;
	}

	public Node getNode() {
		return node;
	}

	public RevocationChecker getRevocationChecker() {
		return revocationChecker;
	}

	public UpdateOverMandatoryManager getUpdateOverMandatory() {
		return uom;
	}

	public ByteCounter getByteCounter() {
		return ctr;
	}

}
