Package freenet.node.updater

Source Code of freenet.node.updater.NodeUpdater

package freenet.node.updater;

import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.SECONDS;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import freenet.client.FetchContext;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.FetchResult;
import freenet.client.async.BinaryBlobWriter;
import freenet.client.async.ClientContext;
import freenet.client.async.ClientGetCallback;
import freenet.client.async.ClientGetter;
import freenet.client.async.PersistenceDisabledException;
import freenet.client.async.USKCallback;
import freenet.keys.FreenetURI;
import freenet.keys.USK;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.RequestClient;
import freenet.node.RequestStarter;
import freenet.node.Version;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.Ticker;
import freenet.support.api.Bucket;
import freenet.support.api.RandomAccessBucket;
import freenet.support.io.Closer;
import freenet.support.io.FileBucket;
import freenet.support.io.FileUtil;
import freenet.support.io.NullOutputStream;

public abstract class NodeUpdater implements ClientGetCallback, USKCallback, RequestClient {

  static private boolean logMINOR;
  private FetchContext ctx;
  private ClientGetter cg;
  private FreenetURI URI;
  private final Ticker ticker;
  public final NodeClientCore core;
  protected final Node node;
  public final NodeUpdateManager manager;
  private final int currentVersion;
  private int realAvailableVersion;
  private int availableVersion;
  private int fetchingVersion;
  protected int fetchedVersion;
  private int maxDeployVersion;
  private int minDeployVersion;
  private boolean isRunning;
  private boolean isFetching;
  private final String blobFilenamePrefix;
  protected File tempBlobFile;
 
  public abstract String jarName();

  NodeUpdater(NodeUpdateManager manager, FreenetURI URI, int current, int min, int max, String blobFilenamePrefix) {
    logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
    this.manager = manager;
    this.node = manager.node;
    this.URI = URI.setSuggestedEdition(Version.buildNumber() + 1);
    this.ticker = node.ticker;
    this.core = node.clientCore;
    this.currentVersion = current;
    this.availableVersion = -1;
    this.isRunning = true;
    this.cg = null;
    this.isFetching = false;
    this.blobFilenamePrefix = blobFilenamePrefix;
    this.maxDeployVersion = max;
    this.minDeployVersion = min;

    FetchContext tempContext = core.makeClient((short) 0, true, false).getFetchContext();
    tempContext.allowSplitfiles = true;
    tempContext.dontEnterImplicitArchives = false;
    this.ctx = tempContext;

  }

  void start() {
    try {
      // because of UoM, this version is actually worth having as well
      USK myUsk = USK.create(URI.setSuggestedEdition(currentVersion));
      core.uskManager.subscribe(myUsk, this, true, getRequestClient());
    } catch(MalformedURLException e) {
      Logger.error(this, "The auto-update URI isn't valid and can't be used");
      manager.blow("The auto-update URI isn't valid and can't be used", true);
    }
  }
 
  protected void maybeProcessOldBlob() {
    File oldBlob = getBlobFile(currentVersion);
    if(oldBlob.exists()) {
      File temp;
      try {
        temp = File.createTempFile(blobFilenamePrefix + availableVersion + "-", ".fblob.tmp", manager.node.clientCore.getPersistentTempDir());
      } catch (IOException e) {
        Logger.error(this, "Unable to process old blob: "+e, e);
        return;
      }
      if(oldBlob.renameTo(temp)) {
        FreenetURI uri = URI.setSuggestedEdition(currentVersion);
        uri = uri.sskForUSK();
        try {
          manager.uom.processMainJarBlob(temp, null, currentVersion, uri);
        } catch (Throwable t) {
          // Don't disrupt startup.
          Logger.error(this, "Unable to process old blob, caught "+t, t);
        }
        temp.delete();
      } else {
        Logger.error(this, "Unable to rename old blob file "+oldBlob+" to "+temp+" so can't process it.");
      }
    }

  }

  public RequestClient getRequestClient() {
    return this;
  }
 
  @Override
  public void onFoundEdition(long l, USK key, ClientContext context, boolean wasMetadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
    if(newKnownGood && !newSlotToo) return;
    logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
    if(logMINOR)
      Logger.minor(this, "Found edition " + l);
    int found;
    synchronized(this) {
      if(!isRunning)
        return;
      found = (int) key.suggestedEdition;

      realAvailableVersion = found;
      if(found > maxDeployVersion) {
        System.err.println("Ignoring "+jarName() + " update edition "+l+": version too new (min "+minDeployVersion+" max "+maxDeployVersion+")");
        found = maxDeployVersion;
      }
     
      if(found <= availableVersion)
        return;
      System.err.println("Found " + jarName() + " update edition " + found);
      Logger.minor(this, "Updating availableVersion from " + availableVersion + " to " + found + " and queueing an update");
      this.availableVersion = found;
    }
    finishOnFoundEdition(found);
  }

  private void finishOnFoundEdition(int found) {
    ticker.queueTimedJob(new Runnable() {
      @Override
      public void run() {
        maybeUpdate();
      }
    }, SECONDS.toMillis(60)); // leave some time in case we get later editions
    // LOCKING: Always take the NodeUpdater lock *BEFORE* the NodeUpdateManager lock
    if(found <= currentVersion) {
      System.err.println("Cancelling fetch for "+found+": not newer than current version "+currentVersion);
      return;
    }
    onStartFetching();
    Logger.minor(this, "Fetching " + jarName() + " update edition " + found);
  }

  protected abstract void onStartFetching();

  public void maybeUpdate() {
    ClientGetter toStart = null;
    if(!manager.isEnabled())
      return;
    if(manager.isBlown())
      return;
    ClientGetter cancelled = null;
    synchronized(this) {
      if(logMINOR)
        Logger.minor(this, "maybeUpdate: isFetching=" + isFetching + ", isRunning=" + isRunning + ", availableVersion=" + availableVersion);
      if(!isRunning)
        return;
      if(isFetching && availableVersion == fetchingVersion)
        return;
      if(availableVersion <= fetchedVersion)
        return;
      if(fetchingVersion < minDeployVersion || fetchingVersion == currentVersion) {
        Logger.normal(this, "Cancelling previous fetch");
        cancelled = cg;
        cg = null;
      }
      fetchingVersion = availableVersion;

      if(availableVersion > currentVersion) {
        Logger.normal(this, "Starting the update process (" + availableVersion + ')');
        System.err.println("Starting the update process: found the update (" + availableVersion + "), now fetching it.");
      }
      if(logMINOR)
        Logger.minor(this, "Starting the update process (" + availableVersion + ')');
      // We fetch it
      try {
        if((cg == null) || cg.isCancelled()) {
          if(logMINOR)
            Logger.minor(this, "Scheduling request for " + URI.setSuggestedEdition(availableVersion));
          if(availableVersion > currentVersion)
            System.err.println("Starting " + jarName() + " fetch for " + availableVersion);
          tempBlobFile =
            File.createTempFile(blobFilenamePrefix + availableVersion + "-", ".fblob.tmp", manager.node.clientCore.getPersistentTempDir());
          FreenetURI uri = URI.setSuggestedEdition(availableVersion);
          uri = uri.sskForUSK();
          cg = new ClientGetter(this, 
            uri, ctx, RequestStarter.IMMEDIATE_SPLITFILE_PRIORITY_CLASS,
            null, new BinaryBlobWriter(new FileBucket(tempBlobFile, false, false, false, false)), null);
          toStart = cg;
        } else {
          System.err.println("Already fetching "+jarName() + " fetch for " + fetchingVersion + " want "+availableVersion);
        }
        isFetching = true;
      } catch(Exception e) {
        Logger.error(this, "Error while starting the fetching: " + e, e);
        isFetching = false;
      }
    }
    if(toStart != null)
      try {
        node.clientCore.clientContext.start(toStart);
      } catch(FetchException e) {
        Logger.error(this, "Error while starting the fetching: " + e, e);
        synchronized(this) {
          isFetching = false;
        }
      } catch (PersistenceDisabledException e) {
        // Impossible
      }
    if(cancelled != null)
      cancelled.cancel(core.clientContext);
  }

  final File getBlobFile(int availableVersion) {
    return new File(node.clientCore.getPersistentTempDir(), blobFilenamePrefix + availableVersion + ".fblob");
  }
 
  RandomAccessBucket getBlobBucket(int availableVersion) {
    File f = getBlobFile(availableVersion);
    if(f == null) return null;
    return new FileBucket(f, true, false, false, false);
  }
 
  @Override
  public void onSuccess(FetchResult result, ClientGetter state) {
    onSuccess(result, state, tempBlobFile, fetchingVersion);
  }

  void onSuccess(FetchResult result, ClientGetter state, File tempBlobFile, int fetchedVersion) {
    logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
    File blobFile = null;
    synchronized(this) {
      if(fetchedVersion <= this.fetchedVersion) {
        tempBlobFile.delete();
        if(result != null) {
          Bucket toFree = result.asBucket();
          if(toFree != null)
            toFree.free();
        }
        return;
      }
      if(result == null || result.asBucket() == null || result.asBucket().size() == 0) {
        tempBlobFile.delete();
        Logger.error(this, "Cannot update: result either null or empty for " + availableVersion);
        System.err.println("Cannot update: result either null or empty for " + availableVersion);
        // Try again
        if(result == null || result.asBucket() == null || availableVersion > fetchedVersion)
          node.ticker.queueTimedJob(new Runnable() {

            @Override
            public void run() {
              maybeUpdate();
            }
          }, 0);
        return;
      }
      blobFile = getBlobFile(fetchedVersion);
      if(!tempBlobFile.renameTo(blobFile)) {
        blobFile.delete();
        if(!tempBlobFile.renameTo(blobFile))
          if(blobFile.exists() && tempBlobFile.exists() &&
            blobFile.length() == tempBlobFile.length())
            Logger.minor(this, "Can't rename " + tempBlobFile + " over " + blobFile + " for " + fetchedVersion + " - probably not a big deal though as the files are the same size");
          else {
            Logger.error(this, "Not able to rename binary blob for node updater: " + tempBlobFile + " -> " + blobFile + " - may not be able to tell other peers about this build");
            blobFile = null;
          }
      }
      this.fetchedVersion = fetchedVersion;
      System.out.println("Found " + jarName() + " version " + fetchedVersion);
      if(fetchedVersion > currentVersion)
        Logger.normal(this, "Found version " + fetchedVersion + ", setting up a new UpdatedVersionAvailableUserAlert");
      maybeParseManifest(result, fetchedVersion);
      this.cg = null;
    }
    processSuccess(fetchedVersion, result, blobFile);
  }
 
  /** We have fetched the jar! Do something after onSuccess(). Called unlocked. */
  protected abstract void processSuccess(int fetched, FetchResult result, File blobFile);

  /** Called with locks held
   * @param result */
  protected abstract void maybeParseManifest(FetchResult result, int build);

  protected void parseManifest(FetchResult result) {
    InputStream is = null;
    try {
      is = result.asBucket().getInputStream();
      ZipInputStream zis = new ZipInputStream(is);
      try {
        ZipEntry ze;
        while(true) {
          ze = zis.getNextEntry();
          if(ze == null) break;
          if(ze.isDirectory()) continue;
          String name = ze.getName();
         
          if(name.equals("META-INF/MANIFEST.MF")) {
            if(logMINOR) Logger.minor(this, "Found manifest");
            long size = ze.getSize();
            if(logMINOR) Logger.minor(this, "Manifest size: "+size);
            if(size > MAX_MANIFEST_SIZE) {
              Logger.error(this, "Manifest is too big: "+size+" bytes, limit is "+MAX_MANIFEST_SIZE);
              break;
            }
            byte[] buf = new byte[(int) size];
            DataInputStream dis = new DataInputStream(zis);
            dis.readFully(buf);
            ByteArrayInputStream bais = new ByteArrayInputStream(buf);
            InputStreamReader isr = new InputStreamReader(bais, "UTF-8");
            BufferedReader br = new BufferedReader(isr);
            String line;
            while((line = br.readLine()) != null) {
              parseManifestLine(line);
            }
          } else {
            zis.closeEntry();
          }
        }
      } finally {
        Closer.close(zis);
      }
    } catch (IOException e) {
      Logger.error(this, "IOException trying to read manifest on update");
    } catch (Throwable t) {
      Logger.error(this, "Failed to parse update manifest: "+t, t);
    } finally {
      Closer.close(is);
    }
  }
 
  static final String DEPENDENCIES_FILE = "dependencies.properties";
 
  /** Read the jar file. Parse the Properties. Read every file in the ZIP; if it is corrupted,
   * we will get a CRC error and therefore an IOException, and so the update won't be deployed.
   * This is not entirely foolproof because ZipInputStream doesn't check the CRC for stored
   * files, only for deflated files, and it's only a CRC32 anyway. But it should reduce the
   * chances of accidental corruption breaking an update.
   * @param is The InputStream for the jar file.
   * @param filename The filename of the manifest file containing the properties (normally
   * META-INF/MANIFEST.MF).
   * @throws IOException If there is a temporary files error or the jar is corrupted. */
  static Properties parseProperties(InputStream is, String filename) throws IOException {
    Properties props = new Properties();
    ZipInputStream zis = new ZipInputStream(is);
    try {
      ZipEntry ze;
      while(true) {
        ze = zis.getNextEntry();
        if(ze == null) break;
        if(ze.isDirectory()) continue;
        String name = ze.getName();
       
        if(name.equals(filename)) {
          if(logMINOR) Logger.minor(NodeUpdater.class, "Found manifest");
          long size = ze.getSize();
          if(logMINOR) Logger.minor(NodeUpdater.class, "Manifest size: "+size);
          if(size > MAX_MANIFEST_SIZE) {
            Logger.error(NodeUpdater.class, "Manifest is too big: "+size+" bytes, limit is "+MAX_MANIFEST_SIZE);
            break;
          }
          byte[] buf = new byte[(int) size];
          DataInputStream dis = new DataInputStream(zis);
          dis.readFully(buf);
          ByteArrayInputStream bais = new ByteArrayInputStream(buf);
          props.load(bais);
        } else {
            // Read the file. Throw if there is a CRC error.
            // Note that java.util.zip.ZipInputStream only checks the CRC for compressed
            // files, so this is not entirely foolproof.
            long size = ze.getSize();
            FileUtil.copy(zis, new NullOutputStream(), size);
          zis.closeEntry();
        }
      }
    } finally {
      Closer.close(zis);
    }
    return props;
  }

  protected void parseDependencies(FetchResult result, int build) {
    InputStream is = null;
    try {
      is = result.asBucket().getInputStream();
      parseDependencies(parseProperties(is, DEPENDENCIES_FILE), build);
    } catch (IOException e) {
      Logger.error(this, "IOException trying to read manifest on update");
    } catch (Throwable t) {
      Logger.error(this, "Failed to parse update manifest: "+t, t);
    } finally {
      Closer.close(is);
    }
  }

  /** Override if you want to deal with the file dependencies.properties */
  protected void parseDependencies(Properties props, int build) {
    // Do nothing
  }

  protected void parseManifestLine(String line) {
    // Do nothing by default, only some NodeUpdater's will use this, those that don't won't call parseManifest().
  }
 
  private static final int MAX_MANIFEST_SIZE = 1024*1024;

  @Override
  public void onFailure(FetchException e, ClientGetter state) {
    logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
    if(!isRunning)
      return;
    FetchExceptionMode errorCode = e.getMode();
    tempBlobFile.delete();

    if(logMINOR)
      Logger.minor(this, "onFailure(" + e + ',' + state + ')');
    synchronized(this) {
      this.cg = null;
      isFetching = false;
    }
    if(errorCode == FetchExceptionMode.CANCELLED ||
      !e.isFatal()) {
      Logger.normal(this, "Rescheduling new request");
      ticker.queueTimedJob(new Runnable() {

        @Override
        public void run() {
          maybeUpdate();
        }
      }, 0);
    } else {
      Logger.error(this, "Canceling fetch : " + e.getMessage());
      System.err.println("Unexpected error fetching update: " + e.getMessage());
      if(e.isFatal()) {
        // Wait for the next version
      } else
        ticker.queueTimedJob(new Runnable() {

          @Override
          public void run() {
            maybeUpdate();
          }
        }, HOURS.toMillis(1));
    }
  }

  /** Called before kill(). Don't do anything that will involve taking locks. */
  public void preKill() {
    isRunning = false;
  }

  void kill() {
    try {
      ClientGetter c;
      synchronized(this) {
        isRunning = false;
        USK myUsk = USK.create(URI.setSuggestedEdition(currentVersion));
        core.uskManager.unsubscribe(myUsk, this);
        c = cg;
        cg = null;
      }
      c.cancel(core.clientContext);
    } catch(Exception e) {
      Logger.minor(this, "Cannot kill NodeUpdater", e);
    }
  }

  public FreenetURI getUpdateKey() {
    return URI;
  }

  public synchronized boolean canUpdateNow() {
    return fetchedVersion > currentVersion;
  }

  /** Called when the fetch URI has changed. No major locks are held by caller.
   * @param uri The new URI. */
  public void onChangeURI(FreenetURI uri) {
    kill();
    this.URI = uri;
    maybeUpdate();
  }

  public int getFetchedVersion() {
    return fetchedVersion;
  }

  public boolean isFetching() {
    return availableVersion > fetchedVersion && availableVersion > currentVersion;
  }

  public int fetchingVersion() {
    // We will not deploy currentVersion...
    if(fetchingVersion <= currentVersion)
      return availableVersion;
    else
      return fetchingVersion;
  }

  public long getBlobSize() {
    return getBlobFile(getFetchedVersion()).length();
  }

  public File getBlobFile() {
    return getBlobFile(getFetchedVersion());
  }

  @Override
  public short getPollingPriorityNormal() {
    return RequestStarter.IMMEDIATE_SPLITFILE_PRIORITY_CLASS;
  }

  @Override
  public short getPollingPriorityProgress() {
    return RequestStarter.INTERACTIVE_PRIORITY_CLASS;
  }

  @Override
  public boolean persistent() {
    return false;
  }

  /**
  ** Called by NodeUpdateManager to re-set the min/max versions for ext when
  ** a new freenet.jar has been downloaded. This is to try to avoid the node
  ** installing incompatible versions of main and ext.
  */
  public void setMinMax(int requiredExt, int recommendedExt) {
    int callFinishedFound = -1;
    synchronized(this) {
      if(recommendedExt > -1) {
        maxDeployVersion = recommendedExt;
      }
      if(requiredExt > -1) {
        minDeployVersion = requiredExt;
        if(realAvailableVersion != availableVersion && availableVersion < requiredExt && realAvailableVersion >= requiredExt) {
          // We found a revision but didn't fetch it because it wasn't within the range for the old jar.
          // The new one requires it, however.
          System.err.println("Previously out-of-range edition "+realAvailableVersion+" is now needed by the new jar; scheduling fetch.");
          callFinishedFound = availableVersion = realAvailableVersion;
        } else if(availableVersion < requiredExt) {
          // Including if it hasn't been found at all
          // Just try it ...
          callFinishedFound = availableVersion = requiredExt;
          System.err.println("Need minimum edition "+requiredExt+" for new jar, found "+availableVersion+"; scheduling fetch.");
        }
      }
    }
    if(callFinishedFound > -1)
      finishOnFoundEdition(callFinishedFound);
  }
 
  @Override
  public boolean realTimeFlag() {
    return false;
  }
 
    @Override
    public void onResume(ClientContext context) {
        // Do nothing. Not persistent.
    }
}
TOP

Related Classes of freenet.node.updater.NodeUpdater

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.