/*
* @(#)JasenAutoUpdater.java 5/01/2005
*
* Copyright (c) 2005 jASEN.org
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the distribution.
*
* 3. The names of the authors may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* 4. Any modification or additions to the software must be contributed back
* to the project.
*
* 5. Any investigation or reverse engineering of source code or binary to
* enable emails to bypass the filters, and hence inflict spam and or viruses
* onto users who use or do not use jASEN could subject the perpetrator to
* criminal and or civil liability.
*
* THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JASEN.ORG,
* OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
package org.jasen.update;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Iterator;
import java.util.Vector;
import java.util.zip.ZipFile;
import org.apache.log4j.Logger;
import org.jasen.JasenScanner;
import org.jasen.interfaces.AutoUpdateExecutor;
import org.jasen.io.ByteArrayReader;
import org.jasen.thread.StoppableThread;
import org.jasen.util.FileUtils;
import org.jasen.util.IOUtils;
import org.jasen.util.WebUtils;
import org.xml.sax.SAXException;
/**
* <p>
* The auto updater is a thread which continuously runs and at regular intervals checks the update site for engine updates.
* </p>
* <p>
* If new updates are found, they are downloaded and upon successful download the engine is signalled to
* block any further scan requests until the updates have been loaded.
* </p>
* @author Jason Polites
*/
public final class JasenAutoUpdater extends StoppableThread {
static final Logger logger = Logger.getLogger(JasenAutoUpdater.class);
public static final String UPDATE_PATH = "updates/";
public static final String UPDATE_LIB_PATH = UPDATE_PATH + "lib/";
public static final String PARCEL_CACHE = UPDATE_PATH + "jasen-update.dat_DO_NOT_DELETE";
private JasenAutoUpdateManager manager;
private long lastUpdateTime = -1L;
private JasenAutoUpdateParcelDigester digester;
private volatile boolean running = false;
private volatile boolean neverStart = false;
private volatile boolean stopped = false;
private volatile boolean forcedUpdate = false;
private volatile boolean idle = true;
/**
*
*/
public JasenAutoUpdater(JasenAutoUpdateManager manager) {
super();
this.manager = manager;
}
/**
* @param name
*/
public JasenAutoUpdater(JasenAutoUpdateManager manager, String name) {
super(name);
this.manager = manager;
}
public void run() {
logger.debug("AutoUpdater starting...");
if(!neverStart) {
running = true;
long nextUpdateTime;
long currentTime;
digester = new JasenAutoUpdateParcelDigester();
digester.init();
// Make sure we have an update path
File updatePath = new File(UPDATE_PATH);
updatePath.mkdirs();
while(running && !neverStart) {
logger.debug("AutoUpdater running");
idle = false;
synchronized(this) {
logger.debug("AutoUpdater in check cycle");
currentTime = System.currentTimeMillis();
if(forcedUpdate) {
forcedUpdate = false;
lastUpdateTime = currentTime;
checkForUpdates();
}
else if(lastUpdateTime == -1 && manager.getConfiguration().isCheckOnStartup()) {
lastUpdateTime = currentTime;
checkForUpdates();
}
else if(lastUpdateTime != -1) {
nextUpdateTime = lastUpdateTime + (manager.getConfiguration().getFrequency() * 60000L); // milliseconds
if(nextUpdateTime <= currentTime) {
lastUpdateTime = currentTime;
checkForUpdates();
}
}
else
{
lastUpdateTime = currentTime;
}
logger.debug("Auto updater is running");
// Wait for timeout or stop signal
if(running) {
try {
idle = true;
logger.debug("AutoUpdater waiting for notification");
wait(manager.getConfiguration().getFrequency() * 60000L);
logger.debug("AutoUpdater stopped waiting");
idle = false;
}
catch (InterruptedException ignore) {}
}
}
}
}
logger.debug("AutoUpdater stopped");
stopped = true;
// Make sure nobody is waiting for us to stop
synchronized(this) {
notifyAll();
}
}
private boolean checkForUpdates() {
logger.debug("Checking for updates...");
// Look for the update parcel
// Because this is small, just write it to a String
ByteArrayOutputStream bout = new ByteArrayOutputStream();
JasenAutoUpdateParcel parcel = null;
JasenAutoUpdateParcel oldParcel = null;
URL parcelUrl;
boolean updated = false;
boolean updateRequired = false;
try {
parcelUrl = prepareItemURL(manager.getConfiguration().getUpdateURL(), manager.getConfiguration().getParcel());
WebUtils.get(parcelUrl, bout, manager.getConfiguration().getReadBuffer(),manager.getConfiguration().getReadTimeout());
// Use the digester to parse the file
ByteArrayReader reader = new ByteArrayReader(bout.toByteArray());
parcel = (JasenAutoUpdateParcel)digester.parse(reader);
// Check the current parcel against the last recorded parcel
oldParcel = loadLastParcel();
if(oldParcel == null || (oldParcel.getUpdateDate() == null || !oldParcel.getUpdateDate().equals(parcel.getUpdateDate()))) {
// We are ok to update...
updateRequired = true;
// Notify the manager
logger.debug("Updates are required, notifying the manager");
manager.notifyUpdateRequired(parcel);
update(parcel);
// And record the update
saveCurrentParcel(parcel);
updated = true;
}
else
{
logger.debug("No updates required");
}
}
catch (IOException e) {
if(e instanceof FileNotFoundException) {
logger.debug("No updates available");
}
else
{
manager.getErrorHandler().handleException(e);
}
}
catch (SAXException e) {
manager.getErrorHandler().handleException(e);
}
catch (ClassNotFoundException e) {
manager.getErrorHandler().handleException(e);
}
finally {
// Notify completed
//if(updateRequired) {
logger.debug("Updates completed, notifying the manager");
// Create a report
JasenAutoUpdateReport report = new JasenAutoUpdateReport();
report.setUpdated(updated);
report.setUpdateParcel(parcel);
if(parcel != null && parcel.getWebUpdateRequired() != null) {
report.setWebUpdateRequired(new Boolean(parcel.getWebUpdateRequired()).booleanValue());
report.setWebUpdateUrl(parcel.getWebUpdateUrl());
}
report.setEngineRestart(updated);
manager.notifyUpdateComplete(report);
logger.debug("Manager notified");
//}
}
return updated;
}
private void update(JasenAutoUpdateParcel parcel) throws IOException {
logger.debug("Updates found, downloading...");
URL updateUrl = prepareItemURL(manager.getConfiguration().getUpdateURL(), parcel.getArchiveName());
// This time, pipe to a file
File update = new File(UPDATE_PATH + parcel.getArchiveName());
FileOutputStream out = null;
try {
out = new FileOutputStream(update);
WebUtils.get(updateUrl, out, manager.getConfiguration().getReadBuffer(),manager.getConfiguration().getReadTimeout(), manager);
}
finally {
if(out != null) {
try {
out.close();
} catch (Exception ignore) {}
}
}
// Now, the update should be a zip file
ZipFile zip = null;
Vector tmpFiles = null;
InputStream zin = null;
OutputStream fout = null;
try {
zip = new ZipFile(update);
// We want to simply extract each file defined in the parcel and write it to the relevant path
if(parcel.getFiles() != null) {
JasenAutoUpdateFile updateFile = null;
Iterator i = parcel.getFiles().iterator();
File outFile = null;
File tmpFile = null;
Vector oldFiles = null;
while(i.hasNext()) {
updateFile = (JasenAutoUpdateFile)i.next();
try {
outFile = new File(updateFile.getJasenPath());
// If this file already exists, rename it until we are done
if(outFile.exists()) {
if(oldFiles == null) {
oldFiles = new Vector();
tmpFiles = new Vector();
}
// Create a temp file so we don't overwrite
tmpFile = new File(outFile.getAbsolutePath() + ".tmp");
tmpFiles.add(tmpFile);
// Copy original to the temp
FileUtils.copy(outFile, tmpFile);
// Save the path of the original for rollback
oldFiles.add(outFile.getAbsolutePath());
}
zin = zip.getInputStream(zip.getEntry(updateFile.getArchivePath()));
fout = new FileOutputStream(outFile);
IOUtils.pipe(zin, fout, 1024);
}
catch (Exception e) {
// We need to rollback the file replacements
if(oldFiles != null) {
if(fout != null) {
try {
fout.close();
fout = null;
} catch (Exception ignore) {}
}
if(zin != null) {
try {
zin.close();
zin = null;
} catch (Exception ignore) {}
}
String path = null;
for (int j = 0; j < oldFiles.size(); j++) {
try {
path = (String)oldFiles.get(j);
outFile = new File(path);
// Get the tmp file
tmpFile = new File(outFile.getAbsolutePath() + ".tmp");
// If the replacment exists, delete it
if(outFile.exists() && tmpFile.exists()) {
outFile.delete();
}
// now move the tmp file back
if(tmpFile.exists()) {
tmpFile.renameTo(outFile);
}
}
catch (Exception ignore) {
manager.getErrorHandler().handleException(ignore);
}
}
}
// Now throw the error back up
if (e instanceof IOException) {
throw (IOException)e;
}
else {
throw new IOException(e.toString());
}
}
finally {
if(fout != null) {
try {
fout.close();
} catch (Exception ignore) {}
}
if(zin != null) {
try {
zin.close();
} catch (Exception ignore) {}
}
}
}
}
// Now, look for classes to execute and jars to load
if(parcel.getJarName() != null) {
// We have a jar to load!
// Save the jar to the given path
File jar = new File(parcel.getJarPath());
File tmpJar = null;
try {
if(jar.exists()) {
// create a copy until the end
tmpJar = new File(jar.getAbsolutePath() + ".tmp");
FileUtils.copy(jar, tmpJar);
if(tmpFiles == null) tmpFiles = new Vector();
tmpFiles.add(tmpJar);
}
else
{
jar.getParentFile().mkdirs();
}
// Now save the zipped jar to the jar path
zin = zip.getInputStream(zip.getEntry(parcel.getJarName()));
fout = new FileOutputStream(jar);
IOUtils.pipe(zin, fout, 1024);
// Now, we need to dynamically load the jar
URL[] jars = new URL[]{jar.toURL()};
ClassLoader parent = JasenScanner.getInstance().getEngine().getContextClassLoader();
if(parent == null) {
parent = this.getContextClassLoader();
}
URLClassLoader loader = new URLClassLoader(jars, parent);
// Set this as the current classloader for the engine
JasenScanner.getInstance().getEngine().setContextClassLoader(loader);
// Now, if there is a class to execute, execute it...
if(parcel.getClassName() != null && parcel.getClassName().trim().length() > 0) {
AutoUpdateExecutor executor = (AutoUpdateExecutor)loader.loadClass(parcel.getClassName()).newInstance();
executor.execute();
}
}
catch (Exception e) {
// Rollback
if(fout != null) {
try {
fout.close();
fout = null;
} catch (Exception ignore) {}
}
if(zin != null) {
try {
zin.close();
zin = null;
} catch (Exception ignore) {}
}
// Move the tmp jar back
if(tmpJar != null) {
if(jar.exists()) {
jar.delete();
}
tmpJar.renameTo(jar);
}
e.printStackTrace();
// Now throw the error back up
if (e instanceof IOException) {
throw (IOException)e;
}
else {
throw new IOException(e.toString());
}
}
finally {
if(fout != null) {
try {
fout.close();
} catch (Exception ignore) {}
}
if(zin != null) {
try {
zin.close();
} catch (Exception ignore) {}
}
}
// Now, if we had a jar and were told NOT to retain it, then we need to delete it...
if(jar != null && jar.exists()) {
if(parcel.getRetainJar().equalsIgnoreCase("false")) {
jar.delete();
}
}
}
// Now we have copied all the files, delete all the tmp files...
if(tmpFiles != null) {
for (int j = 0; j < tmpFiles.size(); j++) {
((File)tmpFiles.get(j)).delete();
}
}
}
finally {
if(zip != null) {
try {
zip.close();
} catch (IOException ignore) {
manager.getErrorHandler().handleException(ignore);
}
}
}
// Now delete the update file itself
if(update != null && update.exists()) {
if(!update.delete()) {
logger.warn("Failed to delete update file at " + update.getAbsolutePath());
}
}
}
private URL prepareItemURL(URL source, String item) throws MalformedURLException {
URL url = null;
String strUrl = source.toExternalForm();
if(!strUrl.endsWith("/")) {
strUrl += "/";
}
strUrl += item;
url = new URL(strUrl);
return url;
}
private JasenAutoUpdateParcel loadLastParcel() throws IOException, ClassNotFoundException {
File parcelFile = new File(PARCEL_CACHE);
JasenAutoUpdateParcel parcel = null;
if(parcelFile.exists()) {
ObjectInputStream in = null;
try {
in = new ObjectInputStream(new FileInputStream(parcelFile));
if(in != null) {
parcel = (JasenAutoUpdateParcel)in.readObject();
}
}
finally {
if(in != null) {
try {
in.close();
} catch (Exception ignore) {}
}
}
}
return parcel;
}
private void saveCurrentParcel(JasenAutoUpdateParcel parcel) throws IOException {
File parcelFile = new File(PARCEL_CACHE);
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(new FileOutputStream(parcelFile));
out.writeObject(parcel);
}
finally {
if(out != null) {
try {
out.close();
} catch (Exception ignore) {}
}
}
}
public synchronized void finish() {
logger.debug("AutoUpdater stopping...");
neverStart = true;
if(running) {
running = false;
}
logger.debug("AutoUpdater stop command completed");
notifyAll();
}
/**
* Forces the auto updater to check for updates immediately
*
*/
synchronized boolean forceUpdate() {
logger.debug("Update has been forced");
if(idle) {
forcedUpdate = true;
notifyAll();
return true;
}
else
{
return false;
}
}
public boolean isRunning() {
return running;
}
public boolean isIdle() {
return idle;
}
public boolean isStopped() {
return stopped;
}
}