All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gemstone.gemfire.internal.JarDeployer Maven / Gradle / Ivy

/*
 * Copyright (c) 2010-2015 Pivotal Software, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License. You
 * may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License. See accompanying
 * LICENSE file.
 */
package com.gemstone.gemfire.internal;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.gemstone.gemfire.LogWriter;
import com.gemstone.gemfire.SystemFailure;

public class JarDeployer implements Serializable {
  private static final long serialVersionUID = 1L;
  public static final String JAR_PREFIX = "vf.gf#";
  private static final Lock lock = new ReentrantLock();

  // Split a versioned filename into its name and version
  public static final Pattern versionedPattern = Pattern.compile("^(.*)#(\\d++)$");

  private final LogWriter logger;

  private final File deployDirectory;

  public JarDeployer() {
    this.logger = null;
    this.deployDirectory = new File(System.getProperty("user.dir"));
  }

  public JarDeployer(final LogWriter logger, final File deployDirectory) {
    this.logger = logger;
    this.deployDirectory = deployDirectory;
  }

  /**
   * Re-deploy all previously deployed JAR files.
   */
  public void loadPreviouslyDeployedJars() {
    List jarClassLoaders = new ArrayList();

    lock.lock();
    try {
      try {
        verifyWritableDeployDirectory();
        final Set jarNames = findDistinctDeployedJars();
        if (!jarNames.isEmpty()) {
          for (String jarName : jarNames) {
            final File[] jarFiles = findSortedOldVersionsOfJar(jarName);

            // It's possible the JARs were deleted by another process
            if (jarFiles.length != 0) {
              JarClassLoader jarClassLoader = findJarClassLoader(jarName);

              try {
                final byte[] jarBytes = getJarContent(jarFiles[0]);
                if (!JarClassLoader.isValidJarContent(jarBytes)) {
                  warning("Invalid JAR file found and deleted: " + jarFiles[0].getAbsolutePath());
                  jarFiles[0].delete();
                } else {
                  // Test to see if the exact same file is already in use
                  if (jarClassLoader == null || !jarClassLoader.getFileName().equals(jarFiles[0].getName())) {
                    jarClassLoader = new JarClassLoader(jarFiles[0], jarName, jarBytes);
                    ClassPathLoader.getLatest().addOrReplaceAndSetLatest(jarClassLoader);
                    jarClassLoaders.add(jarClassLoader);
                  }
                }
              } catch (IOException ioex) {
                // Another process deleted the file so don't bother doing anything else with it
                trace("Failed attempt to use JAR to create JarClassLoader for: " + jarName);
              }

              // Remove any old left-behind versions of this JAR file
              for (File jarFile : jarFiles) {
                if (jarFile.exists() && (jarClassLoader == null || !jarClassLoader.getFileName().equals(jarFile.getName()))) {
                  attemptFileLockAndDelete(jarFile);
                }
              }
            }
          }
        }

        for (JarClassLoader jarClassLoader : jarClassLoaders) {
          jarClassLoader.loadClassesAndRegisterFunctions();
        }
      } catch (VirtualMachineError e) {
        SystemFailure.initiateFailure(e);
        throw e;
      } catch (Throwable th) {
        SystemFailure.checkFailure();
        error("Error when attempting to deploy JAR files on load.", th);
      }
    } finally {
      lock.unlock();
    }
  }

  /**
   * Deploy the given JAR files.
   * 
   * @param jarNames
   *          Array of names of the JAR files to deploy.
   * @param jarBytes
   *          Array of contents of the JAR files to deploy.
   * @return An array of newly created JAR class loaders. Entries will be null for an JARs that were already deployed.
   * @throws IOException
   *           When there's an error saving the JAR file to disk
   */
  public JarClassLoader[] deploy(final String jarNames[], final byte[][] jarBytes) throws IOException, ClassNotFoundException {
    JarClassLoader[] jarClassLoaders = new JarClassLoader[jarNames.length];
    verifyWritableDeployDirectory();

    lock.lock();
    try {
      for (int i = 0; i < jarNames.length; i++) {
        if (!JarClassLoader.isValidJarContent(jarBytes[i])) {
          throw new IllegalArgumentException("File does not contain valid JAR content: " + jarNames[i]);
        }
      }
      
      for (int i = 0; i < jarNames.length; i++) {
        jarClassLoaders[i] = deployWithoutRegistering(jarNames[i], jarBytes[i]);
      }

      for (JarClassLoader jarClassLoader : jarClassLoaders) {
        if (jarClassLoader != null) {
          jarClassLoader.loadClassesAndRegisterFunctions();
        }
      }
    } finally {
      lock.unlock();
    }
    return jarClassLoaders;
  }

  /**
   * Deploy the given JAR file without registering functions.
   * 
   * @param jarName
   *          Name of the JAR file to deploy.
   * @param jarBytes
   *          Contents of the JAR file to deploy.
   * @return The newly created JarClassLoader or null if the JAR was already deployed
   * @throws IOException
   *           When there's an error saving the JAR file to disk
   */
  private JarClassLoader deployWithoutRegistering(final String jarName, final byte[] jarBytes) throws IOException {
    JarClassLoader oldJarClassLoader = findJarClassLoader(jarName);

    trace("Deploying " + jarName
        + (oldJarClassLoader == null ? ": not yet deployed" : ": already deployed as " + oldJarClassLoader.getFileCanonicalPath()));

    // Test to see if the exact same file is being deployed
    if (oldJarClassLoader != null && oldJarClassLoader.hasSameContent(jarBytes)) {
      return null;
    }

    JarClassLoader newJarClassLoader = null;

    do {
      File[] oldJarFiles = findSortedOldVersionsOfJar(jarName);

      try {
        // If this is the first version of this JAR file we've seen ...
        if (oldJarFiles.length == 0) {
          trace("There were no pre-existing versions for JAR: " + jarName);
          File nextVersionJarFile = getNextVersionJarFile(jarName);
          if (writeJarBytesToFile(nextVersionJarFile, jarBytes)) {
            newJarClassLoader = new JarClassLoader(nextVersionJarFile, jarName, jarBytes);
            trace("Successfully created initial JarClassLoader at file: " + nextVersionJarFile.getAbsolutePath());
          } else {
            trace("Unable to write contents for first version of JAR to file: " + nextVersionJarFile.getAbsolutePath());
          }

        } else {
          // Most recent is at the beginning of the list, see if this JAR matches what's
          // already on disk.
          if (doesFileMatchBytes(oldJarFiles[0], jarBytes)) {
            trace("A version on disk was an exact match for the JAR being deployed: " + oldJarFiles[0].getAbsolutePath());
            newJarClassLoader = new JarClassLoader(oldJarFiles[0], jarName, jarBytes);
            trace("Successfully reused JAR to create JarClassLoader from file: " + oldJarFiles[0].getAbsolutePath());
          } else {
            // This JAR isn't on disk
            trace("Need to create a new version for JAR: " + jarName);
            File nextVersionJarFile = getNextVersionJarFile(oldJarFiles[0].getName());
            if (writeJarBytesToFile(nextVersionJarFile, jarBytes)) {
              newJarClassLoader = new JarClassLoader(nextVersionJarFile, jarName, jarBytes);
              trace("Successfully created next JarClassLoader at file: " + nextVersionJarFile.getAbsolutePath());
            } else {
              trace("Unable to write contents for next version of JAR to file: " + nextVersionJarFile.getAbsolutePath());
            }
          }
        }
      } catch (IOException ioex) {
        // Another process deleted the file before we could get to it, just start again
        info("Failed attempt to use JAR to create JarClassLoader for: " + jarName + " : " + ioex.getMessage());
      }

      if (newJarClassLoader == null) {
        trace("Unable to determine a JAR file location, will loop and try again: " + jarName);
      } else {
        trace("Exiting loop for JarClassLoader creation using file: " + newJarClassLoader.getFileName());
      }
    } while (newJarClassLoader == null);

    ClassPathLoader.getLatest().addOrReplaceAndSetLatest(newJarClassLoader);

    // Remove the JAR file that was undeployed as part of this redeploy
    if (oldJarClassLoader != null) {
      attemptFileLockAndDelete(new File(this.deployDirectory, oldJarClassLoader.getFileName()));
    }

    return newJarClassLoader;
  }

  /**
   * Undeploy the given JAR file.
   * 
   * @param jarName
   *          The name of the JAR file to undeploy
   * @return The path to the location on disk where the JAR file had been deployed
   * @throws IOException
   *           If there's a problem deleting the file
   */
  public String undeploy(final String jarName) throws IOException {
    JarClassLoader jarClassLoader = null;
    verifyWritableDeployDirectory();

    lock.lock();
    try {
      jarClassLoader = findJarClassLoader(jarName);
      if (jarClassLoader == null) {
        throw new IllegalArgumentException("JAR not deployed");
      }

      ClassPathLoader.getLatest().removeAndSetLatest(jarClassLoader);
      attemptFileLockAndDelete(new File(this.deployDirectory, jarClassLoader.getFileName()));
      return jarClassLoader.getFileCanonicalPath();
    } finally {
      lock.unlock();
    }
  }

  /**
   * Get a list of all currently deployed JarClassLoaders.
   * 
   * @return The list of JarClassLoaders
   */
  public List findJarClassLoaders() {
    List returnList = new ArrayList();
    Collection classLoaders = ClassPathLoader.getLatest().getClassLoaders();
    for (ClassLoader classLoader : classLoaders) {
      if (classLoader instanceof JarClassLoader) {
        returnList.add((JarClassLoader) classLoader);
      }
    }

    return returnList;
  }

  /**
   * Suspend all deploy and undeploy operations. This is done by acquiring and holding
   * the lock needed in order to perform a deploy or undeploy and so it will cause all
   * threads attempting to do one of these to block. This makes it somewhat of a time
   * sensitive call as forcing these other threads to block for an extended period of
   * time may cause other unforeseen problems.  It must be followed by a call
   * to {@link #resumeAll()}.
   */
  public void suspendAll() {
    lock.lock();
  }
  
  /**
   * Release the lock that controls entry into the deploy/undeploy methods
   * which will allow those activities to continue.
   */
  public void resumeAll() {
    lock.unlock();
  }
  
  /**
   * Figure out the next version of a JAR file
   * 
   * @param latestJarName
   *          The previous most recent version of the JAR file or original name if there wasn't one
   * @return The file that represents the next version
   */
  private File getNextVersionJarFile(final String latestJarName) {
    String newFileName;
    final Matcher matcher = versionedPattern.matcher(latestJarName);
    if (matcher.find()) {
      newFileName = matcher.group(1) + "#" + (Integer.parseInt(matcher.group(2)) + 1);
    } else {
      newFileName = JAR_PREFIX + latestJarName + "#1";
    }

    trace("Next version file name will be:" + newFileName);
    return new File(this.deployDirectory, newFileName);
  }

  /**
   * Attempt to write the given bytes to the given file. If this VM is able to successfully write the contents to the
   * file, or another VM writes the exact same contents, then the write is considered to be successful.
   * 
   * @param file
   *          File of the JAR file to deploy.
   * @param jarBytes
   *          Contents of the JAR file to deploy.
   * @return True if the file was successfully written, false otherwise
   */
  private boolean writeJarBytesToFile(final File file, final byte[] jarBytes) {
    try {
      if (file.createNewFile()) {
        trace("Successfully created new JAR file: " + file.getAbsolutePath());
        final OutputStream outStream = new FileOutputStream(file);
        outStream.write(jarBytes);
        outStream.close();
        return true;
      }
      return doesFileMatchBytes(file, jarBytes);

    } catch (IOException ioex) {
      // Another VM clobbered what was happening here, try again
      trace("IOException while trying to write JAR content to file: " + ioex);
      return false;
    }
  }

  /**
   * Determine if the contents of the file referenced is an exact match for the bytes provided. The method first checks
   * to see if the file is actively being written by checking the length over time. If it appears that the file is
   * actively being written, then it loops waiting for that to complete before doing the comparison.
   * 
   * @param file
   *          File to compare
   * @param bytes
   *          Bytes to compare
   * @return True if there's an exact match, false otherwise
   * @throws IOException
   *           If there's a problem reading the file
   */
  private boolean doesFileMatchBytes(final File file, final byte[] bytes) throws IOException {
    // First check to see if the file is actively being written (if it's not big enough)
    boolean keepTrying = true;
    while (file.length() < bytes.length && keepTrying) {
      trace("Loop waiting for another to write file: " + file.getAbsolutePath());
      long startingFileLength = file.length();
      try {
        Thread.sleep(500);
      } catch (InterruptedException iex) {
        // Just keep looping
      }
      if (startingFileLength == file.length()) {
        trace("Done waiting for another to write file: " + file.getAbsolutePath());
        // Assume the other process has finished writing
        keepTrying = false;
      }
    }

    // If they don't have the same number of bytes then nothing to do
    if (file.length() != bytes.length) {
      trace("Unmatching file length when waiting for another to write file: " + file.getAbsolutePath());
      return false;
    }

    // Open the file then loop comparing each byte
    BufferedInputStream inStream = new BufferedInputStream(new FileInputStream(file));
    int index = 0;
    try {
      for (; index < bytes.length; index++) {
        if (((byte) inStream.read()) != bytes[index]) {
          trace("Did not find a match when waiting for another to write file: " + file.getAbsolutePath());
          return false;
        }
      }
    } finally {
      inStream.close();
    }

    return true;
  }

  private void attemptFileLockAndDelete(final File file) throws IOException {
    FileOutputStream fileOutputStream = new FileOutputStream(file, true);
    try {
      FileLock fileLock = null;
      try {
        fileLock = fileOutputStream.getChannel().tryLock();

        if (fileLock != null) {
          trace("Tried and acquired exclusive lock for file: " + file.getAbsolutePath() + ", w/ channel " + fileLock.channel());

          if (file.delete()) {
            trace("Deleted file with name: " + file.getAbsolutePath());
          } else {
            trace("Could not delete file, will truncate instead and delete on exit: " + file.getAbsolutePath());
            file.deleteOnExit();

            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
            try {
              randomAccessFile.setLength(0);
            } finally {
              try {
                randomAccessFile.close();
              } catch (IOException ioex) {
                error("Could not close file when attempting to set zero length", ioex);
              }
            }
          }
        } else {
          trace("Will not delete file since exclusive lock unavailable: " + file.getAbsolutePath());
        }

      } finally {
        if (fileLock != null) {
          try {
            fileLock.release();
            fileLock.channel().close();
            trace("Released file lock for file: " + file.getAbsolutePath() + ", w/ channel: " + fileLock.channel());
          } catch (IOException ioex) {
            error("Could not close channel on JAR lock file", ioex);
          }
        }
      }
    } finally {
      try {
        fileOutputStream.close();
      } catch (IOException ioex) {
        error("Could not close output stream on JAR file", ioex);
      }
    }
  }

  /**
   * Find the version number that's embedded in the name of this file
   * 
   * @param file
   *          File to get the version number from
   * @return The version number embedded in the filename
   */
  int extractVersionFromFilename(final File file) {
    final Matcher matcher = versionedPattern.matcher(file.getAbsolutePath());
    matcher.find();
    return Integer.parseInt(matcher.group(2));
  }

  private Set findDistinctDeployedJars() {
    final Pattern pattern = Pattern.compile("^" + JAR_PREFIX + "(.*)#\\d++$");

    // Find all deployed JAR files
    final File[] oldFiles = this.deployDirectory.listFiles(new FilenameFilter() {
      @Override
      public boolean accept(final File file, final String name) {
        return pattern.matcher(name).matches();
      }
    });

    // Now add just the original JAR name to the set
    final Set jarNames = new HashSet();
    for (File oldFile : oldFiles) {
      Matcher matcher = pattern.matcher(oldFile.getName());
      matcher.find();
      jarNames.add(matcher.group(1));
    }
    return jarNames;
  }

  /**
   * Find all versions of the JAR file that are currently on disk and return them sorted from newest (highest version)
   * to oldest
   * 
   * @param jarFilename
   *          Name of the JAR file that we want old versions of
   * @return Sorted array of files that are older versions of the given JAR
   */
  private File[] findSortedOldVersionsOfJar(final String jarFilename) {
    // Find all matching files
    final Pattern pattern = Pattern.compile("^" + JAR_PREFIX + jarFilename + "#\\d++$");
    final File[] oldJarFiles = this.deployDirectory.listFiles(new FilenameFilter() {
      @Override
      public boolean accept(final File file, final String name) {
        return (pattern.matcher(name).matches());
      }
    });

    // Sort them in order from newest (highest version) to oldest
    Arrays.sort(oldJarFiles, new Comparator() {
      @Override
      public int compare(final File file1, final File file2) {
        int file1Version = extractVersionFromFilename(file1);
        int file2Version = extractVersionFromFilename(file2);
        return file2Version - file1Version;
      }
    });

    return oldJarFiles;
  }

  private JarClassLoader findJarClassLoader(final String jarName) {
    Collection classLoaders = ClassPathLoader.getLatest().getClassLoaders();
    for (ClassLoader classLoader : classLoaders) {
      if (classLoader instanceof JarClassLoader && ((JarClassLoader) classLoader).getJarName().equals(jarName)) {
        return (JarClassLoader) classLoader;
      }
    }
    return null;
  }

  private void error(final String message, final Throwable throwable) {
    if (this.logger == null) {
      System.err.println(message);
      throwable.printStackTrace(System.err);
    } else {
      this.logger.error(message, throwable);
    }
  }

  private void warning(final String message) {
    if (this.logger == null) {
      System.err.println(message);
    } else {
      this.logger.warning(message);
    }
  }
  
  private void info(final String message) {
    if (this.logger == null) {
      System.out.println(message);
    } else {
      this.logger.info(message);
    }
  }

  private void trace(final String message) {
    if (this.logger == null) {
      System.out.println(message);
    } else {
      this.logger.fine(message);
    }
  }

  /**
   * Make sure that the deploy directory is writable.
   * 
   * @throws IOException
   *           If the directory isn't writable
   */
  private void verifyWritableDeployDirectory() throws IOException {
    Exception exception = null;
    int tryCount = 0;
    do {
      try {
        if (this.deployDirectory.canWrite()) {
          return;
        }
      } catch (Exception ex) {
        exception = ex;
        // We'll just ignore exceptions and loop to try again
      }
      try {
        Thread.sleep(100);
      } catch (InterruptedException iex) {
        error("Interrupted while testing writable deploy directory", iex);
      }
    } while (tryCount++ < 20);

    if (exception != null) {
      throw new IOException("Unable to write to deploy directory", exception);
    }
    throw new IOException("Unable to write to deploy directory");
  }
  
  private byte[] getJarContent(File jarFile) throws IOException {
    InputStream inputStream = new FileInputStream(jarFile);
    try {
      final ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream();
      final byte[] bytes = new byte[4096];

      int bytesRead;
      while (((bytesRead = inputStream.read(bytes)) != -1)) {
        byteOutStream.write(bytes, 0, bytesRead);
      }

      return byteOutStream.toByteArray();
    } finally {
      inputStream.close();
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy