Package freenet.node.updater

Source Code of freenet.node.updater.NodeUpdateManager$UpdaterEnabledCallback

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.Announcer;
import freenet.node.Node;
import freenet.node.NodeInitException;
import freenet.node.NodeStarter;
import freenet.node.OpennetManager;
import freenet.node.PeerNode;
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.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 old key (/update/), which includes the multi-jar
   * updating code, but doesn't require it to work, i.e. it still uses the old
   * freenet-ext.jar and doesn't require any other jars. Older nodes can
   * update to this point via old UOM.
   */
  public final static int TRANSITION_VERSION = 1421;
  /** The freenet-ext.jar build number corresponding to the old key */
  public final static int TRANSITION_VERSION_EXT = 29;

  /** The URI for post-TRANSITION_VERSION builds' freenet.jar. */
  public final static String UPDATE_URI = "USK@sabn9HY9MKLbFPp851AO98uKtsCtYHM9rqB~A5cCGW4,3yps2z06rLnwf50QU4HvsILakRBYd4vBlPtLv0elUts,AQACAAE/jar/"
      + Version.buildNumber();

  /** Might as well be the SSK */
  public final static String LEGACY_UPDATE_URI = "freenet:SSK@BFa1voWr5PunINSZ5BGMqFwhkJTiDBBUrOZ0MYBXseg,BOrxeLzUMb6R9tEZzexymY0zyKAmBNvrU4A9Q0tAqu0,AQACAAE/update-"
      + TRANSITION_VERSION;
  /**
   * Pre-TRANSITION_VERSION builds needed to fetch freenet-ext.jar via an
   * updater of its own.
   */
  public final static String LEGACY_EXT_URI = "freenet:SSK@BFa1voWr5PunINSZ5BGMqFwhkJTiDBBUrOZ0MYBXseg,BOrxeLzUMb6R9tEZzexymY0zyKAmBNvrU4A9Q0tAqu0,AQACAAE/ext-"
      + TRANSITION_VERSION_EXT;

  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 = 16 * 1024 * 1024; // 16MB

  public static final FreenetURI transitionMainJarURI;
  public static final FreenetURI transitionExtJarURI;
 
  public static final FreenetURI transitionMainJarURIAsUSK;
  public static final FreenetURI transitionExtJarURIAsUSK;
 

  public static final String transitionMainJarFilename = "legacy-freenet-jar-"
      + TRANSITION_VERSION + ".fblob";
  public static final String transitionExtJarFilename = "legacy-freenet-ext-jar-"
      + TRANSITION_VERSION_EXT + ".fblob";
 
  public final File transitionMainJarFile;
  public final File transitionExtJarFile;

  static {
    try {
      transitionMainJarURI = new FreenetURI(LEGACY_UPDATE_URI);
      transitionExtJarURI = new FreenetURI(LEGACY_EXT_URI);
      transitionMainJarURIAsUSK = transitionMainJarURI.uskForSSK();
      transitionExtJarURIAsUSK = transitionExtJarURI.uskForSSK();
    } catch (MalformedURLException e) {
      throw new Error(e);
    }
  }

  private FreenetURI updateURI;
  private FreenetURI revocationURI;

  private final LegacyJarFetcher transitionMainJarFetcher;
  private final LegacyJarFetcher transitionExtJarFetcher;

  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;

  public final Node node;

  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;

  public final LegacyUpdateOverMandatoryManager legacyUOM;
  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 = new SubConfig("node.updater", config);

    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");

    updaterConfig
        .register("URI", 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()));
    }
    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()
            && transitionExtJarFetcher.fetched()) {
          System.out.println("Got legacy jars, 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.clientCore.getPersistentTempDir(), transitionMainJarFilename);
    transitionExtJarFile = new File(node.clientCore.getPersistentTempDir(), transitionExtJarFilename);
    transitionMainJarFetcher = new LegacyJarFetcher(transitionMainJarURI,
        transitionMainJarFile, node.clientCore,
        legacyFetcherCallback);
    transitionExtJarFetcher = new LegacyJarFetcher(transitionExtJarURI,
        transitionExtJarFile, node.clientCore,
        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.clientCore.getPersistentTempDir(), "revocation-key.fblob"));

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

  class SimplePuller implements ClientGetCallback {

    final FreenetURI freenetURI;
    final String filename;

    public SimplePuller(FreenetURI freenetURI, String filename) {
      this.freenetURI = freenetURI;
      this.filename = filename;
    }

    public void start(short priority, long maxSize) {
      HighLevelSimpleClient hlsc = node.clientCore.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.clientCore.clientContext.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 = File.createTempFile(filename, ".tmp", node.getRunDir());
        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.renameTo(temp, node.runDir().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.fastWeakRandom.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 "
                + node.getRunDir()
                + " 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.nonPersistentClientBulk;
        }

  }

  public static final String WINDOWS_FILENAME = "freenet-latest-installer-windows.exe";
  public static final String NON_WINDOWS_FILENAME = "freenet-latest-installer-nonwindows.jar";
  public static final String IPV4_TO_COUNTRY_FILENAME = "IpToCountry.dat";

  public File getInstallerWindows() {
    File f = node.runDir().file(WINDOWS_FILENAME);
    if (!(f.exists() && f.canRead() && f.length() > 0))
      return null;
    else
      return f;
  }

  public File getInstallerNonWindows() {
    File f = node.runDir().file(NON_WINDOWS_FILENAME);
    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 getInstallerWindowsURI() {
    return updateURI.sskForUSK().setDocName(
        "installer-" + Version.buildNumber());
  }

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

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

  public void start() throws InvalidConfigValueException {

    node.clientCore.alerts.register(alert);
   
    enable(wasEnabledOnStartup);

    // Fetch 3 files, each to a file in the runDir.

    if (updateSeednodes) {

      SimplePuller seedrefsGetter = new SimplePuller(getSeednodesURI(),
          Announcer.SEEDNODES_FILENAME);
      seedrefsGetter.start(
          RequestStarter.IMMEDIATE_SPLITFILE_PRIORITY_CLASS,
          1024 * 1024);
    }

    if (updateInstallers) {
      SimplePuller installerGetter = new SimplePuller(
          getInstallerWindowsURI(), NON_WINDOWS_FILENAME);
      SimplePuller wininstallerGetter = new SimplePuller(
          getInstallerNonWindowsURI(), WINDOWS_FILENAME);

      installerGetter.start(RequestStarter.UPDATE_PRIORITY_CLASS,
          32 * 1024 * 1024);
      wininstallerGetter.start(RequestStarter.UPDATE_PRIORITY_CLASS,
          32 * 1024 * 1024);

    }

    if (updateIPToCountry) {
      SimplePuller ip4Getter = new SimplePuller(getIPv4ToCountryURI(),
          IPV4_TO_COUNTRY_FILENAME);
      ip4Getter.start(RequestStarter.UPDATE_PRIORITY_CLASS,
          8 * 1024 * 1024);
    }
   
  }

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

  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.peers.localBroadcast(msg, true, true, ctr);
  }

  /** 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();
    boolean extJarAvailable = transitionExtJarFetcher == null ? false
        : transitionExtJarFetcher.fetched();
    return DMT.createUOMAnnounce(transitionMainJarURIAsUSK.toString(),
        transitionExtJarURIAsUSK.toString(),
        revocationURI.toString(), revocationChecker.hasBlown(),
        mainJarAvailable ? TRANSITION_VERSION : -1,
        extJarAvailable ? TRANSITION_VERSION_EXT : -1,
        revocationChecker.lastSucceededDelta(), revocationChecker
            .getRevocationDNFCounter(), revocationChecker
            .getBlobSize(),
        mainJarAvailable ? transitionMainJarFetcher.getBlobSize() : -1,
        extJarAvailable ? transitionExtJarFetcher.getBlobSize() : -1,
        (int) node.nodeStats.getNodeAveragePingTime(),
        (int) node.nodeStats.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.nodeStats.getNodeAveragePingTime(),
        (int) node.nodeStats.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 (sendOld || hasBeenBlown)
        peer.sendAsync(getOldUOMAnnouncement(), null, ctr);
      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();
      transitionExtJarFetcher.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();
      transitionExtJarFetcher.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;
    long minVer = plugin.minimumVersion;
    // But it might already be past that ...
    PluginInfoWrapper info = node.pluginManager.getPluginInfo(name);
    if (info == null) {
      if (!(node.pluginManager.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, Integer.MAX_VALUE, name + "-", name,
        node.pluginManager, 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.clientCore.alerts.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.clientCore.alerts.register(new SimpleUserAlert(false,
          l10n("updateCatastropheTitle"), e.getMessage(),
          l10n("updateCatastropheTitle"), UserAlert.CRITICAL_ERROR));
      return false;
    } catch (UpdaterParserException e) {
      node.clientCore.alerts.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.clientCore.alerts.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.clientCore.alerts.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.clientCore.alerts.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.ticker.queueTimedJob(new Runnable() {
      @Override
      public void run() {
        revocationChecker.start(false);
      }
    }, node.random.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.ticker.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(updateURI.hasMetaStrings())
        throw new InvalidConfigValueException(l10n("updateURIMustHaveNoMetaStrings"));
      if(!updateURI.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.executor.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));
  }

  final ByteCounter ctr = new ByteCounter() {

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

    @Override
    public void sentBytes(int x) {
      node.nodeStats.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.peers.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 getTransitionExtBlob() {
    return transitionExtJarFetcher.getBlobFile();
  }

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

}
TOP

Related Classes of freenet.node.updater.NodeUpdateManager$UpdaterEnabledCallback

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.