diff options
author | Jean-Baptiste Queru <jbq@google.com> | 2013-02-08 15:14:04 -0800 |
---|---|---|
committer | Jean-Baptiste Queru <jbq@google.com> | 2013-02-08 15:14:04 -0800 |
commit | 9edc8f6b58f71ec510ba36b838f115718d9a174d (patch) | |
tree | 06f6df92024fa534ff27e1c0b5fc8b2002848093 /updater | |
parent | b56ea2a18f232d79481e778085fd64e8ae486fc3 (diff) | |
download | idea-9edc8f6b58f71ec510ba36b838f115718d9a174d.tar.gz |
Snapshot of commit 84dc01e773388c2c72a1fc437f313dd5747e7809
from branch master of git://git.jetbrains.org/idea/community.git
Diffstat (limited to 'updater')
22 files changed, 3263 insertions, 0 deletions
diff --git a/updater/src/com/intellij/updater/BaseUpdateAction.java b/updater/src/com/intellij/updater/BaseUpdateAction.java new file mode 100644 index 000000000000..2ddfb685928d --- /dev/null +++ b/updater/src/com/intellij/updater/BaseUpdateAction.java @@ -0,0 +1,85 @@ +package com.intellij.updater; + +import ie.wombat.jbdiff.JBDiff; +import ie.wombat.jbdiff.JBPatch; + +import java.io.*; +import java.util.zip.ZipOutputStream; + +public abstract class BaseUpdateAction extends PatchAction { + public BaseUpdateAction(String path, long checksum) { + super(path, checksum); + } + + public BaseUpdateAction(DataInputStream in) throws IOException { + super(in); + } + + @Override + protected ValidationResult doValidate(File toFile) throws IOException { + ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.UPDATE); + if (result != null) return result; + return doValidateNotChanged(toFile, ValidationResult.Kind.ERROR, ValidationResult.Action.UPDATE); + } + + @Override + protected boolean shouldApplyOn(File toFile) { + // if the file is optional in may not exist + return toFile.exists(); + } + + @Override + protected void doBackup(File toFile, File backupFile) throws IOException { + Utils.copy(toFile, backupFile); + } + + protected void replaceUpdated(File from, File dest) throws IOException { + // on OS X code signing caches seem to be associated with specific file ids, so we need to remove the original file. + if (!dest.delete()) throw new IOException("Cannot delete file " + dest); + Utils.copy(from, dest); + } + + @Override + protected void doRevert(File toFile, File backupFile) throws IOException { + if (!toFile.exists() || isModified(toFile)) { + Utils.copy(backupFile, toFile); + } + } + + protected void writeDiff(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException { + BufferedInputStream olderFileIn = new BufferedInputStream(new FileInputStream(olderFile)); + BufferedInputStream newerFileIn = new BufferedInputStream(new FileInputStream(newerFile)); + try { + writeDiff(olderFileIn, newerFileIn, patchOutput); + } + finally { + olderFileIn.close(); + newerFileIn.close(); + } + } + + protected void writeDiff(InputStream olderFileIn, InputStream newerFileIn, ZipOutputStream patchOutput) + throws IOException { + ByteArrayOutputStream diffOutput = new ByteArrayOutputStream(); + byte[] newerFileBuffer = JBDiff.bsdiff(olderFileIn, newerFileIn, diffOutput); + diffOutput.close(); + + if (diffOutput.size() < newerFileBuffer.length) { + patchOutput.write(1); + Utils.copyBytesToStream(diffOutput, patchOutput); + } + else { + patchOutput.write(0); + Utils.copyBytesToStream(newerFileBuffer, patchOutput); + } + } + + protected void applyDiff(InputStream patchInput, InputStream oldFileIn, OutputStream toFileOut) throws IOException { + if (patchInput.read() == 1) { + JBPatch.bspatch(oldFileIn, toFileOut, patchInput); + } + else { + Utils.copyStream(patchInput, toFileOut); + } + } +} diff --git a/updater/src/com/intellij/updater/ConsoleUpdaterUI.java b/updater/src/com/intellij/updater/ConsoleUpdaterUI.java new file mode 100644 index 000000000000..677502eea7ec --- /dev/null +++ b/updater/src/com/intellij/updater/ConsoleUpdaterUI.java @@ -0,0 +1,40 @@ +package com.intellij.updater; + +import java.util.Map; +import java.util.List; +import java.util.Collections; + +@SuppressWarnings({"UseOfSystemOutOrSystemErr", "CallToPrintStackTrace"}) +public class ConsoleUpdaterUI implements UpdaterUI { + private String myStatus; + + public void startProcess(String title) { + System.out.println(title); + } + + public void setProgress(int percentage) { + } + + public void setProgressIndeterminate() { + } + + public void setStatus(String status) { + System.out.println(myStatus = status); + } + + public void showError(Throwable e) { + e.printStackTrace(); + } + + public void checkCancelled() throws OperationCancelledException { + } + + public Map<String, ValidationResult.Option> askUser(List<ValidationResult> validationResults) { + return Collections.emptyMap(); + } + + @Override + public String toString() { + return "Status: '" + myStatus + '\''; + } +} diff --git a/updater/src/com/intellij/updater/CreateAction.java b/updater/src/com/intellij/updater/CreateAction.java new file mode 100644 index 000000000000..aeeb5ed9d76b --- /dev/null +++ b/updater/src/com/intellij/updater/CreateAction.java @@ -0,0 +1,80 @@ +package com.intellij.updater; + +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class CreateAction extends PatchAction { + public CreateAction(String path) { + super(path, -1); + } + + public CreateAction(DataInputStream in) throws IOException { + super(in); + } + + protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException { + patchOutput.putNextEntry(new ZipEntry(myPath)); + + writeExecutableFlag(patchOutput, newerFile); + Utils.copyFileToStream(newerFile, patchOutput); + + patchOutput.closeEntry(); + } + + @Override + protected ValidationResult doValidate(File toFile) { + ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.CREATE); + if (result != null) return result; + + if (toFile.exists()) { + return new ValidationResult(ValidationResult.Kind.CONFLICT, + myPath, + ValidationResult.Action.CREATE, + ValidationResult.ALREADY_EXISTS_MESSAGE, + ValidationResult.Option.REPLACE, ValidationResult.Option.KEEP); + } + return null; + } + + @Override + protected void doApply(ZipFile patchFile, File toFile) throws IOException { + prepareToWriteFile(toFile); + + InputStream in = Utils.getEntryInputStream(patchFile, myPath); + try { + boolean executable = readExecutableFlag(in); + Utils.copyStreamToFile(in, toFile); + Utils.setExecutable(toFile, executable); + } + finally { + in.close(); + } + } + + private static void prepareToWriteFile(File file) throws IOException { + if (file.exists()) { + Utils.delete(file); + return; + } + + while (file != null && !file.exists()) { + file = file.getParentFile(); + } + if (file != null && !file.isDirectory()) { + Utils.delete(file); + } + } + + protected void doBackup(File toFile, File backupFile) { + // do nothing + } + + protected void doRevert(File toFile, File backupFile) throws IOException { + Utils.delete(toFile); + } +} diff --git a/updater/src/com/intellij/updater/DeleteAction.java b/updater/src/com/intellij/updater/DeleteAction.java new file mode 100644 index 000000000000..3dd1b850e415 --- /dev/null +++ b/updater/src/com/intellij/updater/DeleteAction.java @@ -0,0 +1,57 @@ +package com.intellij.updater; + +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class DeleteAction extends PatchAction { + public DeleteAction(String path, long checksum) { + super(path, checksum); + } + + public DeleteAction(DataInputStream in) throws IOException { + super(in); + } + + @Override + public void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException { + // do nothing + } + + @Override + protected ValidationResult doValidate(File toFile) throws IOException { + ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.DELETE); + if (result != null) return result; + + if (toFile.exists() && isModified(toFile)) { + return new ValidationResult(ValidationResult.Kind.CONFLICT, + myPath, + ValidationResult.Action.DELETE, + "Modified", + ValidationResult.Option.DELETE, + ValidationResult.Option.KEEP); + } + return null; + } + + @Override + protected boolean shouldApplyOn(File toFile) { + return toFile.exists(); + } + + @Override + protected void doApply(ZipFile patchFile, File toFile) throws IOException { + Utils.delete(toFile); + } + + protected void doBackup(File toFile, File backupFile) throws IOException { + Utils.copy(toFile, backupFile); + } + + protected void doRevert(File toFile, File backupFile) throws IOException { + Utils.delete(toFile); // make sure there is no directory remained on this path (may remain from previous 'create' actions + Utils.copy(backupFile, toFile); + } +} diff --git a/updater/src/com/intellij/updater/DiffCalculator.java b/updater/src/com/intellij/updater/DiffCalculator.java new file mode 100644 index 000000000000..4db0033740e9 --- /dev/null +++ b/updater/src/com/intellij/updater/DiffCalculator.java @@ -0,0 +1,42 @@ +package com.intellij.updater; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class DiffCalculator { + public static Result calculate(Map<String, Long> oldChecksums, Map<String, Long> newChecksums) { + Result result = new Result(); + result.filesToDelete = withAllRemoved(oldChecksums, newChecksums); + result.filesToCreate = withAllRemoved(newChecksums, oldChecksums).keySet(); + result.filesToUpdate = collect(oldChecksums, newChecksums, false); + return result; + } + + private static Map<String, Long> withAllRemoved(Map<String, Long> from, Map<String, Long> toRemove) { + Map<String, Long> result = new HashMap<String, Long>(from); + for (String each : toRemove.keySet()) { + result.remove(each); + } + return result; + } + + private static Map<String, Long> collect(Map<String, Long> older, Map<String, Long> newer, boolean equal) { + Map<String, Long> result = new HashMap<String, Long>(); + for (Map.Entry<String, Long> each : newer.entrySet()) { + String file = each.getKey(); + Long oldChecksum = older.get(file); + Long newChecksum = newer.get(file); + if (oldChecksum != null && newChecksum != null && oldChecksum.equals(newChecksum) == equal) { + result.put(file, oldChecksum); + } + } + return result; + } + + public static class Result { + public Map<String, Long> filesToDelete; + public Set<String> filesToCreate; + public Map<String, Long> filesToUpdate; + } +} diff --git a/updater/src/com/intellij/updater/Digester.java b/updater/src/com/intellij/updater/Digester.java new file mode 100644 index 000000000000..326f82961ae0 --- /dev/null +++ b/updater/src/com/intellij/updater/Digester.java @@ -0,0 +1,95 @@ +package com.intellij.updater; + +import java.io.*; +import java.util.*; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class Digester { + public static Map<String, Long> digestFiles(File dir, List<String> ignoredFiles, UpdaterUI ui) + throws IOException, OperationCancelledException { + Map<String, Long> result = new HashMap<String, Long>(); + + LinkedHashSet<String> paths = Utils.collectRelativePaths(dir); + for (String each : paths) { + if (ignoredFiles.contains(each)) continue; + ui.setStatus(each); + ui.checkCancelled(); + result.put(each, digestFile(new File(dir, each))); + } + return result; + } + + public static long digestFile(File file) throws IOException { + if (Utils.isZipFile(file.getName())) { + ZipFile zipFile; + try { + zipFile = new ZipFile(file); + } + catch (IOException e) { + return doDigestRegularFile(file); + } + + try { + return doDigestZipFile(zipFile); + } + finally { + zipFile.close(); + } + } + return doDigestRegularFile(file); + } + + private static long doDigestRegularFile(File file) throws IOException { + InputStream in = new BufferedInputStream(new FileInputStream(file)); + try { + return digestStream(in); + } + finally { + in.close(); + } + } + + private static long doDigestZipFile(ZipFile zipFile) throws IOException { + List<ZipEntry> sorted = new ArrayList<ZipEntry>(); + + Enumeration<? extends ZipEntry> temp = zipFile.entries(); + while (temp.hasMoreElements()) { + ZipEntry each = temp.nextElement(); + if (!each.isDirectory()) sorted.add(each); + } + + Collections.sort(sorted, new Comparator<ZipEntry>() { + public int compare(ZipEntry o1, ZipEntry o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + + CRC32 crc = new CRC32(); + for (ZipEntry each : sorted) { + InputStream in = zipFile.getInputStream(each); + try { + doDigestStream(in, crc); + } + finally { + in.close(); + } + } + return crc.getValue(); + } + + public static long digestStream(InputStream in) throws IOException { + CRC32 crc = new CRC32(); + doDigestStream(in, crc); + return crc.getValue(); + } + + private static void doDigestStream(InputStream in, CRC32 crc) throws IOException { + final byte[] BUFFER = new byte[65536]; + int size; + while ((size = in.read(BUFFER)) != -1) { + crc.update(BUFFER, 0, size); + } + } +} diff --git a/updater/src/com/intellij/updater/OperationCancelledException.java b/updater/src/com/intellij/updater/OperationCancelledException.java new file mode 100644 index 000000000000..bb0cb7c90bbf --- /dev/null +++ b/updater/src/com/intellij/updater/OperationCancelledException.java @@ -0,0 +1,4 @@ +package com.intellij.updater; + +public class OperationCancelledException extends Exception { +} diff --git a/updater/src/com/intellij/updater/Patch.java b/updater/src/com/intellij/updater/Patch.java new file mode 100644 index 000000000000..ac787afd50b0 --- /dev/null +++ b/updater/src/com/intellij/updater/Patch.java @@ -0,0 +1,262 @@ +package com.intellij.updater; + +import java.io.*; +import java.util.*; +import java.util.zip.ZipFile; + +public class Patch { + private List<PatchAction> myActions = new ArrayList<PatchAction>(); + + private static final int CREATE_ACTION_KEY = 1; + private static final int UPDATE_ACTION_KEY = 2; + private static final int UPDATE_ZIP_ACTION_KEY = 3; + private static final int DELETE_ACTION_KEY = 4; + + public Patch(File olderDir, + File newerDir, + List<String> ignoredFiles, + List<String> criticalFiles, + List<String> optionalFiles, + UpdaterUI ui) throws IOException, OperationCancelledException { + calculateActions(olderDir, newerDir, ignoredFiles, criticalFiles, optionalFiles, ui); + } + + public Patch(InputStream patchIn) throws IOException { + read(patchIn); + } + + private void calculateActions(File olderDir, + File newerDir, + List<String> ignoredFiles, + List<String> criticalFiles, + List<String> optionalFiles, + UpdaterUI ui) + throws IOException, OperationCancelledException { + DiffCalculator.Result diff; + + ui.startProcess("Calculating difference..."); + ui.checkCancelled(); + + diff = DiffCalculator.calculate(Digester.digestFiles(olderDir, ignoredFiles, ui), + Digester.digestFiles(newerDir, ignoredFiles, ui)); + + List<PatchAction> tempActions = new ArrayList<PatchAction>(); + + // 'delete' actions before 'create' actions to prevent newly created files to be deleted if the names differ only on case. + for (Map.Entry<String, Long> each : diff.filesToDelete.entrySet()) { + tempActions.add(new DeleteAction(each.getKey(), each.getValue())); + } + + for (String each : diff.filesToCreate) { + tempActions.add(new CreateAction(each)); + } + + for (Map.Entry<String, Long> each : diff.filesToUpdate.entrySet()) { + if (Utils.isZipFile(each.getKey())) { + tempActions.add(new UpdateZipAction(each.getKey(), each.getValue())); + } + else { + tempActions.add(new UpdateAction(each.getKey(), each.getValue())); + } + } + + ui.startProcess("Preparing actions..."); + ui.checkCancelled(); + + for (PatchAction each : tempActions) { + ui.setStatus(each.getPath()); + ui.checkCancelled(); + + if (!each.calculate(olderDir, newerDir)) continue; + myActions.add(each); + each.setCritical(criticalFiles.contains(each.getPath())); + each.setOptional(optionalFiles.contains(each.getPath())); + } + } + + public List<PatchAction> getActions() { + return myActions; + } + + public void write(OutputStream out) throws IOException { + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") DataOutputStream dataOut = new DataOutputStream(out); + try { + dataOut.writeInt(myActions.size()); + + for (PatchAction each : myActions) { + int key; + Class clazz = each.getClass(); + + if (clazz == CreateAction.class) { + key = CREATE_ACTION_KEY; + } + else if (clazz == UpdateAction.class) { + key = UPDATE_ACTION_KEY; + } + else if (clazz == UpdateZipAction.class) { + key = UPDATE_ZIP_ACTION_KEY; + } + else if (clazz == DeleteAction.class) { + key = DELETE_ACTION_KEY; + } + else { + throw new RuntimeException("Unknown action " + each); + } + dataOut.writeInt(key); + each.write(dataOut); + } + } + finally { + dataOut.flush(); + } + } + + private void read(InputStream patchIn) throws IOException { + List<PatchAction> newActions = new ArrayList<PatchAction>(); + + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") DataInputStream in = new DataInputStream(patchIn); + int size = in.readInt(); + + while (size-- > 0) { + int key = in.readInt(); + PatchAction a; + switch (key) { + case CREATE_ACTION_KEY: + a = new CreateAction(in); + break; + case UPDATE_ACTION_KEY: + a = new UpdateAction(in); + break; + case UPDATE_ZIP_ACTION_KEY: + a = new UpdateZipAction(in); + break; + case DELETE_ACTION_KEY: + a = new DeleteAction(in); + break; + default: + throw new RuntimeException("Unknown action type " + key); + } + newActions.add(a); + } + + myActions = newActions; + } + + public List<ValidationResult> validate(final File toDir, UpdaterUI ui) throws IOException, OperationCancelledException { + final LinkedHashSet<String> files = Utils.collectRelativePaths(toDir); + final List<ValidationResult> result = new ArrayList<ValidationResult>(); + + forEach(myActions, "Validating installation...", ui, true, + new ActionsProcessor() { + public void forEach(PatchAction each) throws IOException { + ValidationResult validationResult = each.validate(toDir); + if (validationResult != null) result.add(validationResult); + files.remove(each.getPath()); + } + }); + + //for (String each : files) { + // result.add(new ValidationResult(ValidationResult.Kind.INFO, + // each, + // ValidationResult.Action.NO_ACTION, + // ValidationResult.MANUALLY_ADDED_MESSAGE, + // ValidationResult.Option.KEEP, ValidationResult.Option.DELETE)); + //} + + return result; + } + + public ApplicationResult apply(final ZipFile patchFile, + final File toDir, + final File backupDir, + final Map<String, ValidationResult.Option> options, + UpdaterUI ui) throws IOException, OperationCancelledException { + + List<PatchAction> actionsToProcess = new ArrayList<PatchAction>(); + for (PatchAction each : myActions) { + if (each.shouldApply(toDir, options)) actionsToProcess.add(each); + } + + forEach(actionsToProcess, "Backing up files...", ui, true, + new ActionsProcessor() { + public void forEach(PatchAction each) throws IOException { + each.backup(toDir, backupDir); + } + }); + + final List<PatchAction> appliedActions = new ArrayList<PatchAction>(); + boolean shouldRevert = false; + boolean cancelled = false; + try { + forEach(actionsToProcess, "Applying patch...", ui, true, + new ActionsProcessor() { + public void forEach(PatchAction each) throws IOException { + appliedActions.add(each); + each.apply(patchFile, toDir); + } + }); + } + catch (OperationCancelledException e) { + shouldRevert = true; + cancelled = true; + } + catch (Exception e) { + shouldRevert = true; + ui.showError(e); + } + + if (shouldRevert) { + revert(appliedActions, backupDir, toDir, ui); + appliedActions.clear(); + + if (cancelled) throw new OperationCancelledException(); + } + + // on OS X we need to update bundle timestamp to reset Info.plist caches. + toDir.setLastModified(System.currentTimeMillis()); + + return new ApplicationResult(appliedActions); + } + + public void revert(List<PatchAction> actions, final File backupDir, final File toDir, UpdaterUI ui) + throws OperationCancelledException, IOException { + Collections.reverse(actions); + forEach(actions, "Reverting...", ui, false, + new ActionsProcessor() { + public void forEach(PatchAction each) throws IOException { + each.revert(toDir, backupDir); + } + }); + } + + private static void forEach(List<PatchAction> actions, String title, UpdaterUI ui, boolean canBeCancelled, ActionsProcessor processor) + throws OperationCancelledException, IOException { + ui.startProcess(title); + if (canBeCancelled) ui.checkCancelled(); + + for (int i = 0; i < actions.size(); i++) { + PatchAction each = actions.get(i); + + ui.setStatus(each.getPath()); + if (canBeCancelled) ui.checkCancelled(); + + processor.forEach(each); + + ui.setProgress((i + 1) * 100 / actions.size()); + } + } + + public interface ActionsProcessor { + void forEach(PatchAction each) throws IOException; + } + + public static class ApplicationResult { + final boolean applied; + final List<PatchAction> appliedActions; + + public ApplicationResult(List<PatchAction> appliedActions) { + this.applied = !appliedActions.isEmpty(); + this.appliedActions = appliedActions; + } + } +} diff --git a/updater/src/com/intellij/updater/PatchAction.java b/updater/src/com/intellij/updater/PatchAction.java new file mode 100644 index 000000000000..76259f026bd1 --- /dev/null +++ b/updater/src/com/intellij/updater/PatchAction.java @@ -0,0 +1,203 @@ +package com.intellij.updater; + +import java.io.*; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import java.util.Map; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public abstract class PatchAction { + protected String myPath; + protected long myChecksum; + private boolean isCritical; + private boolean isOptional; + + public PatchAction(String path, long checksum) { + myPath = path; + myChecksum = checksum; + } + + public PatchAction(DataInputStream in) throws IOException { + myPath = in.readUTF(); + myChecksum = in.readLong(); + isCritical = in.readBoolean(); + isOptional = in.readBoolean(); + } + + public void write(DataOutputStream out) throws IOException { + out.writeUTF(myPath); + out.writeLong(myChecksum); + out.writeBoolean(isCritical); + out.writeBoolean(isOptional); + } + + public String getPath() { + return myPath; + } + + protected static void writeExecutableFlag(OutputStream out, File file) throws IOException { + out.write(file.canExecute() ? 1 : 0); + } + + protected static boolean readExecutableFlag(InputStream in) throws IOException { + return in.read() == 1; + } + + public boolean calculate(File olderDir, File newerDir) throws IOException { + return doCalculate(getFile(olderDir), getFile(newerDir)); + } + + protected boolean doCalculate(File olderFile, File newerFile) throws IOException { + return true; + } + + public void buildPatchFile(File olderDir, File newerDir, ZipOutputStream patchOutput) throws IOException { + doBuildPatchFile(getFile(olderDir), getFile(newerDir), patchOutput); + } + + protected abstract void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException; + + public boolean shouldApply(File toDir, Map<String, ValidationResult.Option> options) { + ValidationResult.Option option = options.get(myPath); + if (option == ValidationResult.Option.KEEP || option == ValidationResult.Option.IGNORE) return false; + return shouldApplyOn(getFile(toDir)); + } + + protected boolean shouldApplyOn(File toFile) { + return true; + } + + public ValidationResult validate(File toDir) throws IOException { + return doValidate(getFile(toDir)); + } + + protected abstract ValidationResult doValidate(final File toFile) throws IOException; + + protected ValidationResult doValidateAccess(File toFile, ValidationResult.Action action) { + if (!toFile.exists()) return null; + if (toFile.isDirectory()) return null; + if (toFile.canRead() && toFile.canWrite() && isWritable(toFile)) return null; + return new ValidationResult(ValidationResult.Kind.ERROR, + myPath, + action, + ValidationResult.ACCESS_DENIED_MESSAGE, + ValidationResult.Option.IGNORE); + } + + private boolean isWritable(File toFile) { + try { + FileOutputStream s = new FileOutputStream(toFile, true); + FileChannel ch = s.getChannel(); + try { + FileLock lock = ch.tryLock(); + if (lock == null) return false; + lock.release(); + } + finally { + ch.close(); + s.close(); + } + return true; + } + catch (OverlappingFileLockException e) { + return false; + } + catch (IOException e) { + return false; + } + } + + protected ValidationResult doValidateNotChanged(File toFile, ValidationResult.Kind kind, ValidationResult.Action action) + throws IOException { + if (toFile.exists()) { + if (isModified(toFile)) { + return new ValidationResult(kind, + myPath, + action, + ValidationResult.MODIFIED_MESSAGE, + ValidationResult.Option.IGNORE); + } + } + else if (!isOptional) { + return new ValidationResult(kind, + myPath, + action, + ValidationResult.ABSENT_MESSAGE, + ValidationResult.Option.IGNORE); + } + return null; + } + + protected boolean isModified(File toFile) throws IOException { + return myChecksum != Digester.digestFile(toFile); + } + + public void apply(ZipFile patchFile, File toDir) throws IOException { + doApply(patchFile, getFile(toDir)); + } + + protected abstract void doApply(ZipFile patchFile, File toFile) throws IOException; + + public void backup(File toDir, File backupDir) throws IOException { + doBackup(getFile(toDir), getFile(backupDir)); + } + + protected abstract void doBackup(File toFile, File backupFile) throws IOException; + + public void revert(File toDir, File backupDir) throws IOException { + doRevert(getFile(toDir), getFile(backupDir)); + } + + protected abstract void doRevert(File toFile, File backupFile) throws IOException; + + private File getFile(File baseDir) { + return new File(baseDir, myPath); + } + + public boolean isCritical() { + return isCritical; + } + + public void setCritical(boolean critical) { + isCritical = critical; + } + + public boolean isOptional() { + return isOptional; + } + + public void setOptional(boolean optional) { + isOptional = optional; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + myPath + ", " + myChecksum + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PatchAction that = (PatchAction)o; + + if (isCritical != that.isCritical) return false; + if (isOptional != that.isOptional) return false; + if (myChecksum != that.myChecksum) return false; + if (myPath != null ? !myPath.equals(that.myPath) : that.myPath != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = myPath != null ? myPath.hashCode() : 0; + result = 31 * result + (int)(myChecksum ^ (myChecksum >>> 32)); + result = 31 * result + (isCritical ? 1 : 0); + result = 31 * result + (isOptional ? 1 : 0); + return result; + } +} diff --git a/updater/src/com/intellij/updater/PatchFileCreator.java b/updater/src/com/intellij/updater/PatchFileCreator.java new file mode 100644 index 000000000000..4eab80cfae47 --- /dev/null +++ b/updater/src/com/intellij/updater/PatchFileCreator.java @@ -0,0 +1,115 @@ +package com.intellij.updater; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class PatchFileCreator { + private static final String PATCH_INFO_FILE_NAME = ".patch-info"; + + public static void create(File olderDir, + File newerDir, + File patchFile, + List<String> ignoredFiles, + List<String> criticalFiles, + List<String> optionalFiles, + UpdaterUI ui) throws IOException, OperationCancelledException { + Patch patchInfo = new Patch(olderDir, newerDir, ignoredFiles, criticalFiles, optionalFiles, ui); + ui.startProcess("Creating the patch file '" + patchFile + "'..."); + ui.checkCancelled(); + + ZipOutputStream out = new ZipOutputStream(new FileOutputStream(patchFile)); + try { + out.setLevel(9); + + out.putNextEntry(new ZipEntry(PATCH_INFO_FILE_NAME)); + patchInfo.write(out); + out.closeEntry(); + + List<PatchAction> actions = patchInfo.getActions(); + for (PatchAction each : actions) { + ui.setStatus("Packing " + each.getPath()); + ui.checkCancelled(); + each.buildPatchFile(olderDir, newerDir, out); + } + } + finally { + out.close(); + } + } + + public static PreparationResult prepareAndValidate(File patchFile, + File toDir, + UpdaterUI ui) throws IOException, OperationCancelledException { + Patch patch; + + ZipFile zipFile = new ZipFile(patchFile); + try { + InputStream in = Utils.getEntryInputStream(zipFile, PATCH_INFO_FILE_NAME); + try { + patch = new Patch(in); + } + finally { + in.close(); + } + } + finally { + zipFile.close(); + } + + List<ValidationResult> validationResults = patch.validate(toDir, ui); + return new PreparationResult(patch, patchFile, toDir, validationResults); + } + + public static boolean apply(PreparationResult preparationResult, + Map<String, ValidationResult.Option> options, + UpdaterUI ui) throws IOException, OperationCancelledException { + return apply(preparationResult, options, Utils.createTempDir(), ui).applied; + } + + public static Patch.ApplicationResult apply(PreparationResult preparationResult, + Map<String, ValidationResult.Option> options, + File backupDir, + UpdaterUI ui) throws IOException, OperationCancelledException { + ZipFile zipFile = new ZipFile(preparationResult.patchFile); + try { + return preparationResult.patch.apply(zipFile, preparationResult.toDir, backupDir, options, ui); + } + finally { + zipFile.close(); + } + } + + public static void revert(PreparationResult preparationResult, + List<PatchAction> actionsToRevert, + File backupDir, + UpdaterUI ui) throws IOException, OperationCancelledException { + ZipFile zipFile = new ZipFile(preparationResult.patchFile); + try { + preparationResult.patch.revert(actionsToRevert, backupDir, preparationResult.toDir, ui); + } + finally { + zipFile.close(); + } + } + + public static class PreparationResult { + public Patch patch; + public File patchFile; + public File toDir; + public List<ValidationResult> validationResults; + + public PreparationResult(Patch patch, File patchFile, File toDir, List<ValidationResult> validationResults) { + this.patch = patch; + this.patchFile = patchFile; + this.toDir = toDir; + this.validationResults = validationResults; + } + } +} diff --git a/updater/src/com/intellij/updater/Runner.java b/updater/src/com/intellij/updater/Runner.java new file mode 100644 index 000000000000..e6e02a81fde7 --- /dev/null +++ b/updater/src/com/intellij/updater/Runner.java @@ -0,0 +1,230 @@ +package com.intellij.updater; + +import javax.swing.*; +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +public class Runner { + private static final String PATCH_FILE_NAME = "patch-file.zip"; + private static final String PATCH_PROPERTIES_ENTRY = "patch.properties"; + private static final String OLD_BUILD_DESCRIPTION = "old.build.description"; + private static final String NEW_BUILD_DESCRIPTION = "new.build.description"; + + public static void main(String[] args) throws Exception { + if (args.length != 2 && args.length < 6) { + printUsage(); + return; + } + + String command = args[0]; + + if ("create".equals(command)) { + if (args.length < 6) { + printUsage(); + return; + } + String oldVersionDesc = args[1]; + String newVersionDesc = args[2]; + String oldFolder = args[3]; + String newFolder = args[4]; + String patchFile = args[5]; + List<String> ignoredFiles = extractFiles(args, "ignored"); + List<String> criticalFiles = extractFiles(args, "critical"); + List<String> optionalFiles = extractFiles(args, "optional"); + create(oldVersionDesc, newVersionDesc, oldFolder, newFolder, patchFile, ignoredFiles, criticalFiles, optionalFiles); + } + else if ("install".equals(command)) { + if (args.length != 2) { + printUsage(); + return; + } + + String destFolder = args[1]; + install(destFolder); + } + else { + printUsage(); + return; + } + } + + public static List<String> extractFiles(String[] args, String paramName) { + List<String> result = new ArrayList<String>(); + for (String param : args) { + if (param.startsWith(paramName + "=")) { + param = param.substring((paramName + "=").length()); + for (StringTokenizer tokenizer = new StringTokenizer(param, ";"); tokenizer.hasMoreTokens();) { + String each = tokenizer.nextToken(); + result.add(each); + } + } + } + return result; + } + + private static void printUsage() { + System.err.println("Usage:\n" + + "create <old_version_description> <new_version_description> <old_version_folder> <new_version_folder> <patch_file_name> [ignored=file1;file2;...] [critical=file1;file2;...] [optional=file1;file2;...]\n" + + "install <destination_folder>\n"); + } + + private static void create(String oldBuildDesc, + String newBuildDesc, + String oldFolder, + String newFolder, + String patchFile, + List<String> ignoredFiles, + List<String> criticalFiles, + List<String> optionalFiles) throws IOException, OperationCancelledException { + UpdaterUI ui = new ConsoleUpdaterUI(); + try { + File tempPatchFile = Utils.createTempFile(); + PatchFileCreator.create(new File(oldFolder), + new File(newFolder), + tempPatchFile, + ignoredFiles, + criticalFiles, + optionalFiles, + ui); + + ui.startProcess("Packing jar file '" + patchFile + "'..."); + ZipOutputWrapper out = new ZipOutputWrapper(new FileOutputStream(patchFile)); + try { + ZipInputStream in = new ZipInputStream(new FileInputStream(resolveJarFile())); + try { + ZipEntry e; + while ((e = in.getNextEntry()) != null) { + out.zipEntry(e, in); + } + } + finally { + in.close(); + } + + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + try { + Properties props = new Properties(); + props.put(OLD_BUILD_DESCRIPTION, oldBuildDesc); + props.put(NEW_BUILD_DESCRIPTION, newBuildDesc); + props.store(byteOut, ""); + } + finally { + byteOut.close(); + } + + out.zipBytes(PATCH_PROPERTIES_ENTRY, byteOut); + out.zipFile(PATCH_FILE_NAME, tempPatchFile); + } + finally { + out.close(); + } + } + finally { + cleanup(ui); + } + } + + private static void cleanup(UpdaterUI ui) throws IOException { + ui.startProcess("Cleaning up..."); + ui.setProgressIndeterminate(); + Utils.cleanup(); + } + + private static void install(final String destFolder) throws Exception { + InputStream in = Runner.class.getResourceAsStream("/" + PATCH_PROPERTIES_ENTRY); + Properties props = new Properties(); + try { + props.load(in); + } + finally { + in.close(); + } + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Exception ignore) { + } + + new SwingUpdaterUI(props.getProperty(OLD_BUILD_DESCRIPTION), + props.getProperty(NEW_BUILD_DESCRIPTION), + new SwingUpdaterUI.InstallOperation() { + public boolean execute(UpdaterUI ui) throws OperationCancelledException { + return doInstall(ui, destFolder); + } + }); + } + + private static boolean doInstall(UpdaterUI ui, String destFolder) throws OperationCancelledException { + try { + try { + File patchFile = Utils.createTempFile(); + ZipFile jarFile = new ZipFile(resolveJarFile()); + + ui.startProcess("Extracting patch file..."); + ui.setProgressIndeterminate(); + try { + InputStream in = Utils.getEntryInputStream(jarFile, PATCH_FILE_NAME); + OutputStream out = new BufferedOutputStream(new FileOutputStream(patchFile)); + try { + Utils.copyStream(in, out); + } + finally { + in.close(); + out.close(); + } + } + finally { + jarFile.close(); + } + + ui.checkCancelled(); + + File destDir = new File(destFolder); + PatchFileCreator.PreparationResult result = PatchFileCreator.prepareAndValidate(patchFile, destDir, ui); + Map<String, ValidationResult.Option> options = ui.askUser(result.validationResults); + return PatchFileCreator.apply(result, options, ui); + } + catch (IOException e) { + ui.showError(e); + } + } + finally { + try { + cleanup(ui); + } + catch (IOException e) { + ui.showError(e); + } + } + + return false; + } + + private static File resolveJarFile() throws IOException { + URL url = Runner.class.getResource(""); + if (url == null) throw new IOException("Cannot resolve jar file path"); + if (!"jar".equals(url.getProtocol())) throw new IOException("Patch file is not a 'jar' file"); + + String path = url.getPath(); + + int start = path.indexOf("file:/"); + int end = path.indexOf("!/"); + if (start == -1 || end == -1) throw new IOException("Unknown protocol: " + url); + + String jarFileUrl = path.substring(start, end); + + try { + return new File(new URI(jarFileUrl)); + } + catch (URISyntaxException e) { + throw new IOException(e.getMessage()); + } + } +} diff --git a/updater/src/com/intellij/updater/SwingUpdaterUI.java b/updater/src/com/intellij/updater/SwingUpdaterUI.java new file mode 100644 index 000000000000..7d28d444519a --- /dev/null +++ b/updater/src/com/intellij/updater/SwingUpdaterUI.java @@ -0,0 +1,559 @@ +package com.intellij.updater; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableColumn; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SwingUpdaterUI implements UpdaterUI { + private static final int RESULT_REQUIRES_RESTART = 42; + + private static final EmptyBorder FRAME_BORDER = new EmptyBorder(8, 8, 8, 8); + private static final EmptyBorder LABEL_BORDER = new EmptyBorder(0, 0, 5, 0); + private static final EmptyBorder BUTTONS_BORDER = new EmptyBorder(5, 0, 0, 0); + + private static final String TITLE = "Update"; + + private static final String CANCEL_BUTTON_TITLE = "Cancel"; + private static final String EXIT_BUTTON_TITLE = "Exit"; + + private static final String PROCEED_BUTTON_TITLE = "Proceed"; + + private final InstallOperation myOperation; + + private final JLabel myProcessTitle; + private final JProgressBar myProcessProgress; + private final JLabel myProcessStatus; + private final JTextArea myConsole; + private final JPanel myConsolePane; + + private final JButton myCancelButton; + + private final ConcurrentLinkedQueue<UpdateRequest> myQueue = new ConcurrentLinkedQueue<UpdateRequest>(); + private final AtomicBoolean isCancelled = new AtomicBoolean(false); + private final AtomicBoolean isRunning = new AtomicBoolean(false); + private final AtomicBoolean hasError = new AtomicBoolean(false); + private final JFrame myFrame; + private boolean myApplied; + + public SwingUpdaterUI(String oldBuildDesc, String newBuildDesc, InstallOperation operation) { + myOperation = operation; + + myProcessTitle = new JLabel(" "); + myProcessProgress = new JProgressBar(0, 100); + myProcessStatus = new JLabel(" "); + + myCancelButton = new JButton(CANCEL_BUTTON_TITLE); + + myConsole = new JTextArea(); + myConsole.setLineWrap(true); + myConsole.setWrapStyleWord(true); + myConsole.setCaretPosition(myConsole.getText().length()); + myConsole.setTabSize(1); + myConsolePane = new JPanel(new BorderLayout()); + myConsolePane.add(new JScrollPane(myConsole)); + myConsolePane.setBorder(BUTTONS_BORDER); + myConsolePane.setVisible(false); + + myCancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + doCancel(); + } + }); + + myFrame = new JFrame(); + myFrame.setTitle(TITLE); + + myFrame.setLayout(new BorderLayout()); + myFrame.getRootPane().setBorder(FRAME_BORDER); + myFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + + myFrame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + doCancel(); + } + }); + + JPanel processPanel = new JPanel(); + processPanel.setLayout(new BoxLayout(processPanel, BoxLayout.Y_AXIS)); + processPanel.add(myProcessTitle); + processPanel.add(myProcessProgress); + processPanel.add(myProcessStatus); + + processPanel.add(myConsolePane); + for (Component each : processPanel.getComponents()) { + ((JComponent)each).setAlignmentX(Component.LEFT_ALIGNMENT); + } + + JPanel buttonsPanel = new JPanel(); + buttonsPanel.setBorder(BUTTONS_BORDER); + buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS)); + buttonsPanel.add(Box.createHorizontalGlue()); + buttonsPanel.add(myCancelButton); + + myProcessTitle.setText("<html>Updating " + oldBuildDesc + " to " + newBuildDesc + "..."); + + myFrame.add(processPanel, BorderLayout.CENTER); + myFrame.add(buttonsPanel, BorderLayout.SOUTH); + + myFrame.setMinimumSize(new Dimension(500, 50)); + myFrame.pack(); + + centerOnScreen(myFrame); + + myFrame.setVisible(true); + + myQueue.add(new UpdateRequest() { + @Override + public void perform() { + doPerform(); + } + }); + + startRequestDispatching(); + } + + private void startRequestDispatching() { + new Thread(new Runnable() { + public void run() { + while (true) { + try { + Thread.sleep(100); + } + catch (InterruptedException e) { + return; + } + + final List<UpdateRequest> pendingRequests = new ArrayList<UpdateRequest>(); + UpdateRequest request; + while ((request = myQueue.poll()) != null) { + pendingRequests.add(request); + } + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + for (UpdateRequest each : pendingRequests) { + each.perform(); + } + } + }); + } + } + }).start(); + } + + private static void centerOnScreen(Window frame) { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + frame.setLocation((screenSize.width - frame.getWidth()) / 2, + (screenSize.height - frame.getHeight()) / 2); + } + + private void doCancel() { + if (isRunning.get()) { + int result = JOptionPane.showConfirmDialog(myFrame, + "The patch has not been applied yet.\nAre you sure you want to abort the operation?", + TITLE, JOptionPane.YES_NO_OPTION); + if (result == JOptionPane.YES_OPTION) { + isCancelled.set(true); + myCancelButton.setEnabled(false); + } + } + else { + exit(); + } + } + + private void doPerform() { + isRunning.set(true); + + new Thread(new Runnable() { + public void run() { + try { + myApplied = myOperation.execute(SwingUpdaterUI.this); + } + catch (OperationCancelledException ignore) { + } + finally { + isRunning.set(false); + + if (hasError.get()) { + startProcess("Failed to apply patch"); + setProgress(100); + myCancelButton.setText(EXIT_BUTTON_TITLE); + myCancelButton.setEnabled(true); + } else { + exit(); + } + } + } + }).start(); + } + + private void exit() { + System.exit(myApplied ? RESULT_REQUIRES_RESTART : 0); + } + + public Map<String, ValidationResult.Option> askUser(final List<ValidationResult> validationResults) throws OperationCancelledException { + if (validationResults.isEmpty()) return Collections.emptyMap(); + + final Map<String, ValidationResult.Option> result = new HashMap<String, ValidationResult.Option>(); + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + final JDialog dialog = new JDialog(myFrame, TITLE, true); + dialog.setLayout(new BorderLayout()); + dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); + + JPanel buttonsPanel = new JPanel(); + buttonsPanel.setBorder(BUTTONS_BORDER); + buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS)); + buttonsPanel.add(Box.createHorizontalGlue()); + JButton proceedButton = new JButton(PROCEED_BUTTON_TITLE); + proceedButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + dialog.setVisible(false); + } + }); + + JButton cancelButton = new JButton(CANCEL_BUTTON_TITLE); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + isCancelled.set(true); + myCancelButton.setEnabled(false); + dialog.setVisible(false); + } + }); + + buttonsPanel.add(proceedButton); + buttonsPanel.add(cancelButton); + + dialog.getRootPane().setDefaultButton(proceedButton); + + JTable table = new JTable(); + + table.setCellSelectionEnabled(true); + table.setDefaultEditor(ValidationResult.Option.class, new MyCellEditor()); + table.setDefaultRenderer(Object.class, new MyCellRenderer()); + MyTableModel model = new MyTableModel(validationResults); + table.setModel(model); + + for (int i = 0; i < table.getColumnModel().getColumnCount(); i++) { + TableColumn each = table.getColumnModel().getColumn(i); + each.setPreferredWidth(MyTableModel.getColumnWidth(i, new Dimension(600, 400).width)); + } + + String message = "<html>There are some conflicts found in the installation.<br><br>" + + "Please select desired solutions from the " + MyTableModel.COLUMNS[MyTableModel.OPTIONS_COLUMN_INDEX] + + " column and press " + PROCEED_BUTTON_TITLE + ".<br>" + + "If you do not want to proceed with the update, please press " + CANCEL_BUTTON_TITLE + ".</html>"; + + JLabel label = new JLabel(message); + label.setBorder(LABEL_BORDER); + dialog.add(label, BorderLayout.NORTH); + dialog.add(new JScrollPane(table), BorderLayout.CENTER); + dialog.add(buttonsPanel, BorderLayout.SOUTH); + + dialog.getRootPane().setBorder(FRAME_BORDER); + + dialog.setSize(new Dimension(600, 400)); + centerOnScreen(dialog); + dialog.setVisible(true); + + result.putAll(model.getResult()); + } + }); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + checkCancelled(); + return result; + } + + public void startProcess(final String title) { + myQueue.add(new UpdateRequest() { + public void perform() { + myProcessStatus.setText(title); + myProcessProgress.setIndeterminate(false); + myProcessProgress.setValue(0); + } + }); + } + + public void setProgress(final int percentage) { + myQueue.add(new UpdateRequest() { + public void perform() { + myProcessProgress.setIndeterminate(false); + myProcessProgress.setValue(percentage); + } + }); + } + + public void setProgressIndeterminate() { + myQueue.add(new UpdateRequest() { + public void perform() { + myProcessProgress.setIndeterminate(true); + } + }); + } + + public void setStatus(final String status) { + } + + public void showError(final Throwable e) { + myQueue.add(new UpdateRequest() { + public void perform() { + StringWriter w = new StringWriter(); + e.printStackTrace(new PrintWriter(w)); + w.append("\n"); + myConsole.append(w.getBuffer().toString()); + if (!myConsolePane.isVisible()) { + myConsolePane.setVisible(true); + myConsolePane.setPreferredSize(new Dimension(10, 200)); + myFrame.pack(); + } + hasError.set(true); + } + }); + } + + public void checkCancelled() throws OperationCancelledException { + if (isCancelled.get()) throw new OperationCancelledException(); + } + + public interface InstallOperation { + boolean execute(UpdaterUI ui) throws OperationCancelledException; + } + + private interface UpdateRequest { + void perform(); + } + + public static void main(String[] args) { + new SwingUpdaterUI("xxx", "yyy", new InstallOperation() { + public boolean execute(UpdaterUI ui) throws OperationCancelledException { + ui.startProcess("Process1"); + ui.checkCancelled(); + for (int i = 0; i < 200; i++) { + ui.setStatus("i = " + i); + ui.checkCancelled(); + try { + Thread.sleep(10); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + ui.setProgress((i + 1) * 100 / 200); + } + + ui.showError(new Throwable()); + + ui.startProcess("Process3"); + ui.checkCancelled(); + ui.setProgressIndeterminate(); + try { + for (int i = 0; i < 200; i++) { + ui.setStatus("i = " + i); + ui.checkCancelled(); + try { + Thread.sleep(10); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + ui.setProgress((i + 1) * 100 / 200); + if (i == 100) { + List<ValidationResult> vr = new ArrayList<ValidationResult>(); + vr.add(new ValidationResult(ValidationResult.Kind.ERROR, + "foo/bar", + ValidationResult.Action.CREATE, + "Hello", + ValidationResult.Option.REPLACE, + ValidationResult.Option.KEEP)); + vr.add(new ValidationResult(ValidationResult.Kind.CONFLICT, + "foo/bar/baz", + ValidationResult.Action.DELETE, + "World", + ValidationResult.Option.DELETE, + ValidationResult.Option.KEEP)); + vr.add(new ValidationResult(ValidationResult.Kind.INFO, + "xxx", + ValidationResult.Action.NO_ACTION, + "bla-bla", + ValidationResult.Option.IGNORE)); + ui.askUser(vr); + } + } + } + finally { + ui.startProcess("Process2"); + for (int i = 0; i < 200; i++) { + ui.setStatus("i = " + i); + try { + Thread.sleep(10); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + ui.setProgress((i + 1) * 100 / 200); + } + } + return true; + } + }); + } + + private static class MyTableModel extends AbstractTableModel { + public static final String[] COLUMNS = new String[]{"File", "Action", "Problem", "Solution"}; + public static final int OPTIONS_COLUMN_INDEX = 3; + private final List<Item> myItems = new ArrayList<Item>(); + + public MyTableModel(List<ValidationResult> validationResults) { + for (ValidationResult each : validationResults) { + myItems.add(new Item(each, each.options.get(0))); + } + } + + public int getColumnCount() { + return COLUMNS.length; + } + + @Override + public String getColumnName(int column) { + return COLUMNS[column]; + } + + public static int getColumnWidth(int column, int totalWidth) { + switch (column) { + case 0: + return (int)(totalWidth * 0.6); + default: + return (int)(totalWidth * 0.15); + } + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + if (columnIndex == OPTIONS_COLUMN_INDEX) { + return ValidationResult.Option.class; + } + return super.getColumnClass(columnIndex); + } + + public int getRowCount() { + return myItems.size(); + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return columnIndex == OPTIONS_COLUMN_INDEX && getOptions(rowIndex).size() > 1; + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + if (columnIndex == OPTIONS_COLUMN_INDEX) { + myItems.get(rowIndex).option = (ValidationResult.Option)value; + } + } + + public Object getValueAt(int rowIndex, int columnIndex) { + Item item = myItems.get(rowIndex); + switch (columnIndex) { + case 0: + return item.validationResult.path; + case 1: + return item.validationResult.action; + case 2: + return item.validationResult.message; + case OPTIONS_COLUMN_INDEX: + return item.option; + } + return null; + } + + public ValidationResult.Kind getKind(int rowIndex) { + return myItems.get(rowIndex).validationResult.kind; + } + + public List<ValidationResult.Option> getOptions(int rowIndex) { + Item item = myItems.get(rowIndex); + return item.validationResult.options; + } + + public Map<String, ValidationResult.Option> getResult() { + Map<String, ValidationResult.Option> result = new HashMap<String, ValidationResult.Option>(); + for (Item each : myItems) { + result.put(each.validationResult.path, each.option); + } + return result; + } + + private static class Item { + ValidationResult validationResult; + ValidationResult.Option option; + + private Item(ValidationResult validationResult, ValidationResult.Option option) { + this.validationResult = validationResult; + this.option = option; + } + } + } + + private static class MyCellEditor extends DefaultCellEditor { + public MyCellEditor() { + super(new JComboBox()); + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + MyTableModel tableModel = (MyTableModel)table.getModel(); + DefaultComboBoxModel comboModel = new DefaultComboBoxModel(); + + for (ValidationResult.Option each : tableModel.getOptions(row)) { + comboModel.addElement(each); + } + ((JComboBox)editorComponent).setModel(comboModel); + + return super.getTableCellEditorComponent(table, value, isSelected, row, column); + } + } + + private static class MyCellRenderer extends DefaultTableCellRenderer { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + Component result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (!isSelected) { + MyTableModel tableModel = (MyTableModel)table.getModel(); + Color color = table.getBackground(); + + switch (tableModel.getKind(row)) { + case ERROR: + color = new Color(255, 175, 175); + break; + case CONFLICT: + color = new Color(255, 240, 240); + break; + } + result.setBackground(color); + } + return result; + } + } +} diff --git a/updater/src/com/intellij/updater/UpdateAction.java b/updater/src/com/intellij/updater/UpdateAction.java new file mode 100644 index 000000000000..6c79c44cbc4b --- /dev/null +++ b/updater/src/com/intellij/updater/UpdateAction.java @@ -0,0 +1,48 @@ +package com.intellij.updater; + +import java.io.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class UpdateAction extends BaseUpdateAction { + public UpdateAction(String path, long checksum) { + super(path, checksum); + } + + public UpdateAction(DataInputStream in) throws IOException { + super(in); + } + + @Override + protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException { + patchOutput.putNextEntry(new ZipEntry(myPath)); + writeExecutableFlag(patchOutput, newerFile); + writeDiff(olderFile, newerFile, patchOutput); + patchOutput.closeEntry(); + } + + @Override + protected void doApply(ZipFile patchFile, File toFile) throws IOException { + InputStream in = Utils.findEntryInputStream(patchFile, myPath); + boolean executable = readExecutableFlag(in); + + File temp = Utils.createTempFile(); + OutputStream out = new BufferedOutputStream(new FileOutputStream(temp)); + try { + InputStream oldFileIn = new FileInputStream(toFile); + try { + applyDiff(in, oldFileIn, out); + } + finally { + oldFileIn.close(); + } + } + finally { + out.close(); + } + + replaceUpdated(temp, toFile); + Utils.setExecutable(toFile, executable); + } +} diff --git a/updater/src/com/intellij/updater/UpdateZipAction.java b/updater/src/com/intellij/updater/UpdateZipAction.java new file mode 100644 index 000000000000..553e7d8da0a4 --- /dev/null +++ b/updater/src/com/intellij/updater/UpdateZipAction.java @@ -0,0 +1,246 @@ +package com.intellij.updater; + +import java.io.*; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class UpdateZipAction extends BaseUpdateAction { + Set<String> myFilesToCreate; + Set<String> myFilesToUpdate; + Set<String> myFilesToDelete; + + public UpdateZipAction(String path, long checksum) { + super(path, checksum); + } + + // test support + public UpdateZipAction(String path, + Collection<String> filesToCreate, + Collection<String> filesToUpdate, + Collection<String> filesToDelete, + long checksum) { + super(path, checksum); + myFilesToCreate = new HashSet<String>(filesToCreate); + myFilesToUpdate = new HashSet<String>(filesToUpdate); + myFilesToDelete = new HashSet<String>(filesToDelete); + } + + public UpdateZipAction(DataInputStream in) throws IOException { + super(in); + + int count = in.readInt(); + myFilesToCreate = new HashSet<String>(count); + while (count-- > 0) { + myFilesToCreate.add(in.readUTF()); + } + + count = in.readInt(); + myFilesToUpdate = new HashSet<String>(count); + while (count-- > 0) { + myFilesToUpdate.add(in.readUTF()); + } + + count = in.readInt(); + myFilesToDelete = new HashSet<String>(count); + while (count-- > 0) { + myFilesToDelete.add(in.readUTF()); + } + } + + @Override + public void write(DataOutputStream out) throws IOException { + super.write(out); + + out.writeInt(myFilesToCreate.size()); + for (String each : myFilesToCreate) { + out.writeUTF(each); + } + + out.writeInt(myFilesToUpdate.size()); + for (String each : myFilesToUpdate) { + out.writeUTF(each); + } + + out.writeInt(myFilesToDelete.size()); + for (String each : myFilesToDelete) { + out.writeUTF(each); + } + } + + @Override + protected boolean doCalculate(File olderFile, File newerFile) throws IOException { + final Map<String, Long> oldCheckSums = new HashMap<String, Long>(); + final Map<String, Long> newCheckSums = new HashMap<String, Long>(); + + processZipFile(olderFile, new Processor() { + public void process(ZipEntry entry, InputStream in) throws IOException { + oldCheckSums.put(entry.getName(), Digester.digestStream(in)); + } + }); + + processZipFile(newerFile, new Processor() { + public void process(ZipEntry entry, InputStream in) throws IOException { + newCheckSums.put(entry.getName(), Digester.digestStream(in)); + } + }); + + DiffCalculator.Result diff = DiffCalculator.calculate(oldCheckSums, newCheckSums); + + myFilesToCreate = diff.filesToCreate; + myFilesToUpdate = diff.filesToUpdate.keySet(); + myFilesToDelete = diff.filesToDelete.keySet(); + + return !(myFilesToCreate.isEmpty() && myFilesToUpdate.isEmpty() && myFilesToDelete.isEmpty()); + } + + @Override + public void doBuildPatchFile(final File olderFile, final File newerFile, final ZipOutputStream patchOutput) throws IOException { + try { + //noinspection IOResourceOpenedButNotSafelyClosed + new ZipFile(newerFile).close(); + } + catch (IOException e) { + throw new IOException("Corrupted target file: " + newerFile, e); + } + + final Set<String> filesToProcess = new HashSet<String>(myFilesToCreate); + filesToProcess.addAll(myFilesToUpdate); + if (filesToProcess.isEmpty()) return; + + final ZipFile olderZip; + try { + olderZip = new ZipFile(olderFile); + } + catch (IOException e) { + throw new IOException("Corrupted source file: " + olderFile, e); + } + + try { + processZipFile(newerFile, new Processor() { + public void process(ZipEntry newerEntry, InputStream newerEntryIn) throws IOException { + if (newerEntry.isDirectory()) return; + String name = newerEntry.getName(); + if (!filesToProcess.contains(name)) return; + + try { + patchOutput.putNextEntry(new ZipEntry(myPath + "/" + name)); + InputStream olderEntryIn = Utils.findEntryInputStream(olderZip, name); + if (olderEntryIn == null) { + Utils.copyStream(newerEntryIn, patchOutput); + } + else { + writeDiff(olderEntryIn, newerEntryIn, patchOutput); + } + patchOutput.closeEntry(); + } + catch (IOException e) { + throw new IOException("Error building patch for .zip entry " + name, e); + } + } + }); + } + finally { + olderZip.close(); + } + } + + protected void doApply(final ZipFile patchFile, File toFile) throws IOException { + File temp = Utils.createTempFile(); + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") + final ZipOutputWrapper out = new ZipOutputWrapper(new FileOutputStream(temp)); + out.setCompressionLevel(0); + + try { + processZipFile(toFile, new Processor() { + public void process(ZipEntry entry, InputStream in) throws IOException { + String path = entry.getName(); + if (myFilesToDelete.contains(path)) return; + + if (myFilesToUpdate.contains(path)) { + OutputStream entryOut = out.zipStream(path); + try { + applyDiff(Utils.findEntryInputStream(patchFile, myPath + "/" + path), in, entryOut); + } + finally { + entryOut.close(); + } + } + else { + out.zipEntry(entry, in); + } + } + }); + + for (String each : myFilesToCreate) { + InputStream in = Utils.getEntryInputStream(patchFile, myPath + "/" + each); + try { + out.zipEntry(each, in); + } + finally { + in.close(); + } + } + } + finally { + out.close(); + } + + replaceUpdated(temp, toFile); + } + + private static void processZipFile(File file, Processor processor) throws IOException { + ZipInputStream in = new ZipInputStream(new FileInputStream(file)); + try { + ZipEntry inEntry; + Set<String> processed = new HashSet<String>(); + while ((inEntry = in.getNextEntry()) != null) { + if (inEntry.isDirectory()) continue; + if (processed.contains(inEntry.getName())) { + throw new IOException("Duplicate entry '" + inEntry.getName() + "' in " + file.getPath()); + } + //noinspection IOResourceOpenedButNotSafelyClosed + processor.process(inEntry, new BufferedInputStream(in)); + processed.add(inEntry.getName()); + } + } + finally { + in.close(); + } + } + + private interface Processor { + void process(ZipEntry entry, InputStream in) throws IOException; + } + + @Override + public String toString() { + return super.toString() + myFilesToCreate + " " + myFilesToUpdate + " " + myFilesToDelete; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + UpdateZipAction that = (UpdateZipAction)o; + + if (myFilesToCreate != null ? !myFilesToCreate.equals(that.myFilesToCreate) : that.myFilesToCreate != null) return false; + if (myFilesToUpdate != null ? !myFilesToUpdate.equals(that.myFilesToUpdate) : that.myFilesToUpdate != null) return false; + if (myFilesToDelete != null ? !myFilesToDelete.equals(that.myFilesToDelete) : that.myFilesToDelete != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (myFilesToCreate != null ? myFilesToCreate.hashCode() : 0); + result = 31 * result + (myFilesToUpdate != null ? myFilesToUpdate.hashCode() : 0); + result = 31 * result + (myFilesToDelete != null ? myFilesToDelete.hashCode() : 0); + return result; + } +} diff --git a/updater/src/com/intellij/updater/UpdaterUI.java b/updater/src/com/intellij/updater/UpdaterUI.java new file mode 100644 index 000000000000..85005ff81205 --- /dev/null +++ b/updater/src/com/intellij/updater/UpdaterUI.java @@ -0,0 +1,20 @@ +package com.intellij.updater; + +import java.util.List; +import java.util.Map; + +public interface UpdaterUI { + void startProcess(String title); + + void setProgress(int percentage); + + void setProgressIndeterminate(); + + void setStatus(String status); + + void showError(Throwable e); + + void checkCancelled() throws OperationCancelledException; + + Map<String, ValidationResult.Option> askUser(List<ValidationResult> validationResults) throws OperationCancelledException; +}
\ No newline at end of file diff --git a/updater/src/com/intellij/updater/Utils.java b/updater/src/com/intellij/updater/Utils.java new file mode 100644 index 000000000000..427788eaf1ac --- /dev/null +++ b/updater/src/com/intellij/updater/Utils.java @@ -0,0 +1,177 @@ +package com.intellij.updater; + +import java.io.*; +import java.util.LinkedHashSet; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class Utils { + // keep buffer static as there may be many calls of the copyStream method. + private static final byte[] BUFFER = new byte[64 * 1024]; + private static File myTempDir; + + public static boolean isZipFile(String fileName) { + return fileName.endsWith(".zip") || fileName.endsWith(".jar"); + } + + @SuppressWarnings({"SSBasedInspection"}) + public static File createTempFile() throws IOException { + if (myTempDir == null) { + myTempDir = File.createTempFile("idea.updater", "tmp"); + delete(myTempDir); + myTempDir.mkdirs(); + } + + return File.createTempFile("temp", "tmp", myTempDir); + } + + public static File createTempDir() throws IOException { + File result = createTempFile(); + delete(result); + result.mkdirs(); + return result; + } + + public static void cleanup() throws IOException { + if (myTempDir == null) return; + delete(myTempDir); + myTempDir = null; + } + + public static void delete(File file) throws IOException { + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) { + for (File each : files) { + delete(each); + } + } + } + for (int i = 0; i < 10; i++) { + if (file.delete() || !file.exists()) return; + try { + Thread.sleep(10); + } + catch (InterruptedException ignore) { + } + } + if (file.exists()) throw new IOException("Cannot delete file " + file); + } + + public static void setExecutable(File file, boolean executable) throws IOException { + if (executable && !file.setExecutable(true)) { + throw new IOException("Cannot set executable permissions for: " + file); + } + } + + public static void copy(File from, File to) throws IOException { + if (from.isDirectory()) { + File[] files = from.listFiles(); + if (files == null) throw new IOException("Cannot get directory's content: " + from); + for (File each : files) { + copy(each, new File(to, each.getName())); + } + } + else { + InputStream in = new BufferedInputStream(new FileInputStream(from)); + try { + copyStreamToFile(in, to); + } + finally { + in.close(); + } + setExecutable(to, from.canExecute()); + } + } + + public static void copyFileToStream(File from, OutputStream out) throws IOException { + InputStream in = new BufferedInputStream(new FileInputStream(from)); + try { + copyStream(in, out); + } + finally { + in.close(); + } + } + + public static void copyStreamToFile(InputStream from, File to) throws IOException { + to.getParentFile().mkdirs(); + OutputStream out = new BufferedOutputStream(new FileOutputStream(to)); + try { + copyStream(from, out); + } + finally { + out.close(); + } + } + + public static void copyBytesToStream(ByteArrayOutputStream from, OutputStream to) throws IOException { + OutputStream out = new BufferedOutputStream(to); + try { + from.writeTo(out); + } + finally { + out.flush(); + } + } + + public static void copyBytesToStream(byte[] bytes, OutputStream to) throws IOException { + to.write(bytes); + } + + public static byte[] readBytes(InputStream in) throws IOException { + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + try { + copyStream(in, byteOut); + } + finally { + byteOut.close(); + } + return byteOut.toByteArray(); + } + + public static void copyStream(InputStream in, OutputStream out) throws IOException { + while (true) { + int read = in.read(BUFFER); + if (read < 0) break; + out.write(BUFFER, 0, read); + } + } + + public static InputStream getEntryInputStream(ZipFile zipFile, String entryPath) throws IOException { + InputStream result = findEntryInputStream(zipFile, entryPath); + if (result == null) throw new IOException("Entry " + entryPath + " not found"); + return result; + } + + public static InputStream findEntryInputStream(ZipFile zipFile, String entryPath) throws IOException { + ZipEntry entry = zipFile.getEntry(entryPath); + if (entry == null || entry.isDirectory()) return null; + + // if isDirectory check failed, check presence of 'file/' manually + if (!entryPath.endsWith("/") && zipFile.getEntry(entryPath + "/") != null) return null; + + return new BufferedInputStream(zipFile.getInputStream(entry)); + } + + public static LinkedHashSet<String> collectRelativePaths(File dir) { + LinkedHashSet<String> result = new LinkedHashSet<String>(); + collectRelativePaths(dir, result, null); + return result; + } + + private static void collectRelativePaths(File dir, LinkedHashSet<String> result, String parentPath) { + File[] children = dir.listFiles(); + if (children == null) return; + + for (File each : children) { + String relativePath = (parentPath == null ? "" : parentPath + "/") + each.getName(); + if (each.isDirectory()) { + collectRelativePaths(each, result, relativePath); + } + else { + result.add(relativePath); + } + } + } +} diff --git a/updater/src/com/intellij/updater/ValidationResult.java b/updater/src/com/intellij/updater/ValidationResult.java new file mode 100644 index 000000000000..02c142446152 --- /dev/null +++ b/updater/src/com/intellij/updater/ValidationResult.java @@ -0,0 +1,94 @@ +package com.intellij.updater; + +import java.util.Arrays; +import java.util.List; + +public class ValidationResult implements Comparable<ValidationResult> { + public enum Kind { + INFO, CONFLICT, ERROR + } + + public enum Action { + CREATE("Create"), UPDATE("Update"), DELETE("Delete"), NO_ACTION(""); + + private final String myDisplayString; + + Action(String displayString) { + myDisplayString = displayString; + } + + @Override + public String toString() { + return myDisplayString; + } + } + + public enum Option { + IGNORE, KEEP, REPLACE, DELETE + } + + public static final String ABSENT_MESSAGE = "Absent"; + public static final String MODIFIED_MESSAGE = "Modified"; + public static final String ACCESS_DENIED_MESSAGE = "Access denied"; + public static final String ALREADY_EXISTS_MESSAGE = "Already exists"; + + public final Kind kind; + public final String path; + public final Action action; + public final String message; + public final List<Option> options; + + public ValidationResult(Kind kind, String path, Action action, String message, Option... options) { + this.kind = kind; + this.path = path; + this.action = action; + this.message = message; + this.options = Arrays.asList(options); + } + + @Override + public String toString() { + String prefix; + switch (kind) { + case CONFLICT: + prefix = "?"; + break; + case ERROR: + prefix = "!"; + break; + default: + prefix = ""; + } + return prefix + action + " " + path + ": " + message + " (" + options + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ValidationResult result = (ValidationResult)o; + + if (action != result.action) return false; + if (kind != result.kind) return false; + if (message != null ? !message.equals(result.message) : result.message != null) return false; + if (options != null ? !options.equals(result.options) : result.options != null) return false; + if (path != null ? !path.equals(result.path) : result.path != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = kind != null ? kind.hashCode() : 0; + result = 31 * result + (path != null ? path.hashCode() : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + (options != null ? options.hashCode() : 0); + return result; + } + + public int compareTo(ValidationResult o) { + return path.compareToIgnoreCase(o.path); + } +} diff --git a/updater/src/com/intellij/updater/ZipOutputWrapper.java b/updater/src/com/intellij/updater/ZipOutputWrapper.java new file mode 100644 index 000000000000..1ccbc45ff05b --- /dev/null +++ b/updater/src/com/intellij/updater/ZipOutputWrapper.java @@ -0,0 +1,143 @@ +package com.intellij.updater; + +import java.io.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class ZipOutputWrapper { + private final ZipOutputStream myOut; + private final Set<String> myDirs = new HashSet<String>(); + private boolean isCompressed = true; + + public ZipOutputWrapper(OutputStream stream) { + myOut = new ZipOutputStream(new BufferedOutputStream(stream)); + } + + public void setCompressionLevel(int level) { + myOut.setLevel(level); + if (level == 0) { + myOut.setMethod(ZipEntry.STORED); + isCompressed = false; + } + } + + public OutputStream zipStream(final String entryPath) throws IOException { + final ByteArrayOutputStream tempOut = new ByteArrayOutputStream(); + return new BufferedOutputStream(new OutputStream() { + @Override + public void write(int b) throws IOException { + tempOut.write(b); + } + + @Override + public void close() throws IOException { + super.close(); + tempOut.close(); + zipBytes(entryPath, tempOut); + } + }); + } + + public void zipEntry(ZipEntry entry, InputStream from) throws IOException { + if (entry.isDirectory()) { + addDirs(entry.getName(), true); + return; + } + zipEntry(entry.getName(), from); + } + + public void zipEntry(String entryPath, InputStream from) throws IOException { + ByteArrayOutputStream tempOut = new ByteArrayOutputStream(); + try { + Utils.copyStream(from, tempOut); + } + finally { + tempOut.close(); + } + zipBytes(entryPath, tempOut); + } + + public void zipBytes(String entryPath, ByteArrayOutputStream byteOut) throws IOException { + addDirs(entryPath, false); + + ZipEntry entry = new ZipEntry(entryPath); + if (!isCompressed) { + entry.setSize(byteOut.size()); + CRC32 crc = new CRC32(); + crc.update(byteOut.toByteArray()); + entry.setCrc(crc.getValue()); + } + + myOut.putNextEntry(entry); + try { + byteOut.writeTo(myOut); + } + finally { + myOut.closeEntry(); + } + } + + public void zipFile(String entryPath, File file) throws IOException { + if (file.isDirectory()) { + addDirs(entryPath, true); + return; + } + + InputStream from = new BufferedInputStream(new FileInputStream(file)); + try { + zipEntry(new ZipEntry(entryPath), from); + } + finally { + from.close(); + } + } + + public void zipFiles(File dir) throws IOException { + for (File each : dir.listFiles()) { + addFileToZip(each, null); + } + } + + private void addFileToZip(File file, String parentPath) throws IOException { + String path = parentPath == null ? file.getName() : parentPath + "/" + file.getName(); + zipFile(path, file); + + if (file.isDirectory()) { + for (File each : file.listFiles()) { + addFileToZip(each, path); + } + } + } + + private void addDirs(String relPath, boolean isDir) { + List<String> temp = new ArrayList<String>(); + if (isDir && !relPath.endsWith("/")) relPath += "/"; + int index = 0; + while ((index = relPath.indexOf('/', index + 1)) != -1) { + temp.add(relPath.substring(0, index)); + } + myDirs.addAll(temp); + } + + public void close() throws IOException { + try { + for (String each : myDirs) { + if (!each.endsWith("/")) each += "/"; + ZipEntry e = new ZipEntry(each); + e.setMethod(ZipEntry.STORED); + e.setSize(0); + e.setCrc(0); + myOut.putNextEntry(e); + myOut.closeEntry(); + } + } + finally { + myOut.close(); + } + } +} diff --git a/updater/src/ie/wombat/jbdiff/JBDiff.java b/updater/src/ie/wombat/jbdiff/JBDiff.java new file mode 100644 index 000000000000..92ddfd13e50a --- /dev/null +++ b/updater/src/ie/wombat/jbdiff/JBDiff.java @@ -0,0 +1,487 @@ +/* +* Copyright (c) 2005, Joe Desbonnet, (jdesbonnet@gmail.com) +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * 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. +* * Neither the name of the <organization> nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY <copyright holder> ``AS IS'' AND ANY +* EXPRESS 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 <copyright holder> 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 ie.wombat.jbdiff; + +import com.intellij.updater.Utils; + +import java.io.*; +import java.util.zip.GZIPOutputStream; + +/** + * Java Binary Diff utility. Based on + * bsdiff (v4.2) by Colin Percival + * (see http://www.daemonology.net/bsdiff/ ) and distributed under BSD license. + * <p/> + * <p> + * Running this on large files will probably require an increae of the default + * maximum heap size (use java -Xmx200m) + * </p> + * + * @author Joe Desbonnet, jdesbonnet@gmail.com + */ + +public class JBDiff { + + private static final String VERSION = "jbdiff-0.1.1"; + + private static final int min(int x, int y) { + return x < y ? x : y; + } + + private final static void split(int[] I, int[] V, int start, int len, int h) { + + int i, j, k, x, tmp, jj, kk; + + if (len < 16) { + for (k = start; k < start + len; k += j) { + j = 1; + x = V[I[k] + h]; + for (i = 1; k + i < start + len; i++) { + if (V[I[k + i] + h] < x) { + x = V[I[k + i] + h]; + j = 0; + } + + if (V[I[k + i] + h] == x) { + tmp = I[k + j]; + I[k + j] = I[k + i]; + I[k + i] = tmp; + j++; + } + } + + for (i = 0; i < j; i++) { + V[I[k + i]] = k + j - 1; + } + if (j == 1) { + I[k] = -1; + } + } + + return; + } + + x = V[I[start + len / 2] + h]; + jj = 0; + kk = 0; + for (i = start; i < start + len; i++) { + if (V[I[i] + h] < x) { + jj++; + } + if (V[I[i] + h] == x) { + kk++; + } + } + + jj += start; + kk += jj; + + i = start; + j = 0; + k = 0; + while (i < jj) { + if (V[I[i] + h] < x) { + i++; + } + else if (V[I[i] + h] == x) { + tmp = I[i]; + I[i] = I[jj + j]; + I[jj + j] = tmp; + j++; + } + else { + tmp = I[i]; + I[i] = I[kk + k]; + I[kk + k] = tmp; + k++; + } + } + + while (jj + j < kk) { + if (V[I[jj + j] + h] == x) { + j++; + } + else { + tmp = I[jj + j]; + I[jj + j] = I[kk + k]; + I[kk + k] = tmp; + k++; + } + } + + if (jj > start) { + split(I, V, start, jj - start, h); + } + + for (i = 0; i < kk - jj; i++) { + V[I[jj + i]] = kk - 1; + } + + if (jj == kk - 1) { + I[jj] = -1; + } + + if (start + len > kk) { + split(I, V, kk, start + len - kk, h); + } + } + + /** + * Fast suffix sporting. + * Larsson and Sadakane's qsufsort algorithm. + * See http://www.cs.lth.se/Research/Algorithms/Papers/jesper5.ps + * + * @param I + * @param V + * @param oldBuf + */ + private static void qsufsort(int[] I, int[] V, byte[] oldBuf) { + + int oldsize = oldBuf.length; + + int[] buckets = new int[256]; + int i, h, len; + + for (i = 0; i < 256; i++) { + buckets[i] = 0; + } + + for (i = 0; i < oldsize; i++) { + buckets[(int)oldBuf[i] & 0xff]++; + } + + for (i = 1; i < 256; i++) { + buckets[i] += buckets[i - 1]; + } + + for (i = 255; i > 0; i--) { + buckets[i] = buckets[i - 1]; + } + + buckets[0] = 0; + + for (i = 0; i < oldsize; i++) { + I[++buckets[(int)oldBuf[i] & 0xff]] = i; + } + + I[0] = oldsize; + for (i = 0; i < oldsize; i++) { + V[i] = buckets[(int)oldBuf[i] & 0xff]; + } + V[oldsize] = 0; + + for (i = 1; i < 256; i++) { + if (buckets[i] == buckets[i - 1] + 1) { + I[buckets[i]] = -1; + } + } + + I[0] = -1; + + for (h = 1; I[0] != -(oldsize + 1); h += h) { + len = 0; + for (i = 0; i < oldsize + 1;) { + if (I[i] < 0) { + len -= I[i]; + i -= I[i]; + } + else { + //if(len) I[i-len]=-len; + if (len != 0) { + I[i - len] = -len; + } + len = V[I[i]] + 1 - i; + split(I, V, i, len, h); + i += len; + len = 0; + } + } + + if (len != 0) { + I[i - len] = -len; + } + } + + for (i = 0; i < oldsize + 1; i++) { + I[V[i]] = i; + } + } + + /** + * Count the number of bytes that match in oldBuf (starting at offset oldOffset) + * and newBuf (starting at offset newOffset). + * + * @param oldBuf + * @param oldOffset + * @param newBuf + * @param newOffset + * @return + */ + private final static int matchlen(byte[] oldBuf, int oldOffset, byte[] newBuf, + int newOffset) { + int end = min(oldBuf.length - oldOffset, newBuf.length - newOffset); + int i; + for (i = 0; i < end; i++) { + if (oldBuf[oldOffset + i] != newBuf[newOffset + i]) { + break; + } + } + return i; + } + + private final static int search(int[] I, byte[] oldBuf, byte[] newBuf, + int newBufOffset, int start, int end, IntByRef pos) { + int x, y; + + if (end - start < 2) { + x = matchlen(oldBuf, I[start], newBuf, newBufOffset); + y = matchlen(oldBuf, I[end], newBuf, newBufOffset); + + if (x > y) { + pos.value = I[start]; + return x; + } + else { + pos.value = I[end]; + return y; + } + } + + x = start + (end - start) / 2; + if (Util.memcmp(oldBuf, I[x], newBuf, newBufOffset) < 0) { + return search(I, oldBuf, newBuf, newBufOffset, x, end, pos); + } + else { + return search(I, oldBuf, newBuf, newBufOffset, start, x, pos); + } + } + + public static byte[] bsdiff(InputStream oldFileIn, InputStream newFileIn, OutputStream diffFileOut) + throws IOException { + + byte[] oldBuf = Utils.readBytes(oldFileIn); + int oldsize = oldBuf.length; + + int[] I = new int[oldsize + 1]; + int[] V = new int[oldsize + 1]; + + qsufsort(I, V, oldBuf); + + //free(V) + V = null; + System.gc(); + + byte[] newBuf = Utils.readBytes(newFileIn); + int newsize = newBuf.length; + + // diff block + int dblen = 0; + byte[] db = new byte[newsize]; + + // extra block + int eblen = 0; + byte[] eb = new byte[newsize]; + + /* + * Diff file is composed as follows: + * + * Header (32 bytes) + * Data (from offset 32 to end of file) + * + * Header: + * Offset 0, length 8 bytes: file magic "jbdiff40" + * Offset 8, length 8 bytes: length of ctrl block + * Offset 16, length 8 bytes: length of compressed diff block + * Offset 24, length 8 bytes: length of new file + * + * Data: + * 32 (length ctrlBlockLen): ctrlBlock + * 32+ctrlBlockLen (length diffBlockLen): diffBlock (gziped) + * 32+ctrlBlockLen+diffBlockLen (to end of file): extraBlock (gziped) + * + * ctrlBlock comprises a set of records, each record 12 bytes. A record + * comprises 3 x 32 bit integers. The ctrlBlock is not compressed. + */ + + ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(); + DataOutputStream diffOut = new DataOutputStream(arrayOut); + + int oldscore, scsc; + + int overlap, Ss, lens; + int i; + int scan = 0; + int len = 0; + int lastscan = 0; + int lastpos = 0; + int lastoffset = 0; + + IntByRef pos = new IntByRef(); + int ctrlBlockLen = 0; + + while (scan < newsize) { + + oldscore = 0; + + for (scsc = scan += len; scan < newsize; scan++) { + + len = search(I, oldBuf, newBuf, scan, 0, oldsize, pos); + + for (; scsc < scan + len; scsc++) { + if ((scsc + lastoffset < oldsize) + && (oldBuf[scsc + lastoffset] == newBuf[scsc])) { + oldscore++; + } + } + + if (((len == oldscore) && (len != 0)) || (len > oldscore + 8)) { + break; + } + + if ((scan + lastoffset < oldsize) + && (oldBuf[scan + lastoffset] == newBuf[scan])) { + oldscore--; + } + } + + if ((len != oldscore) || (scan == newsize)) { + int s = 0; + int Sf = 0; + int lenf = 0; + for (i = 0; (lastscan + i < scan) && (lastpos + i < oldsize);) { + if (oldBuf[lastpos + i] == newBuf[lastscan + i]) { + s++; + } + i++; + if (s * 2 - i > Sf * 2 - lenf) { + Sf = s; + lenf = i; + } + } + + int lenb = 0; + if (scan < newsize) { + s = 0; + int Sb = 0; + for (i = 1; (scan >= lastscan + i) && (pos.value >= i); i++) { + if (oldBuf[pos.value - i] == newBuf[scan - i]) { + s++; + } + if (s * 2 - i > Sb * 2 - lenb) { + Sb = s; + lenb = i; + } + } + } + + if (lastscan + lenf > scan - lenb) { + overlap = (lastscan + lenf) - (scan - lenb); + s = 0; + Ss = 0; + lens = 0; + for (i = 0; i < overlap; i++) { + if (newBuf[lastscan + lenf - overlap + i] == oldBuf[lastpos + + lenf - overlap + i]) { + s++; + } + if (newBuf[scan - lenb + i] == oldBuf[pos.value - lenb + i]) { + s--; + } + if (s > Ss) { + Ss = s; + lens = i + 1; + } + } + + lenf += lens - overlap; + lenb -= lens; + } + + // ? byte casting introduced here -- might affect things + for (i = 0; i < lenf; i++) { + db[dblen + i] = (byte)(newBuf[lastscan + i] - oldBuf[lastpos + + i]); + } + + for (i = 0; i < (scan - lenb) - (lastscan + lenf); i++) { + eb[eblen + i] = newBuf[lastscan + lenf + i]; + } + + dblen += lenf; + eblen += (scan - lenb) - (lastscan + lenf); + + /* + * Write control block entry (3 x int) + */ + diffOut.writeInt(lenf); + diffOut.writeInt((scan - lenb) - (lastscan + lenf)); + diffOut.writeInt((pos.value - lenb) - (lastpos + lenf)); + ctrlBlockLen += 12; + + lastscan = scan - lenb; + lastpos = pos.value - lenb; + lastoffset = pos.value - scan; + } // end if + } // end while loop + + GZIPOutputStream gzOut; + + /* + * Write diff block + */ + gzOut = new GZIPOutputStream(diffOut); + gzOut.write(db, 0, dblen); + gzOut.finish(); + int diffBlockLen = diffOut.size() - ctrlBlockLen; + + /* + * Write extra block + */ + gzOut = new GZIPOutputStream(diffOut); + gzOut.write(eb, 0, eblen); + gzOut.finish(); + + diffOut.close(); + + DataOutputStream headerStream = new DataOutputStream(diffFileOut); + headerStream.write("jbdiff40".getBytes("US-ASCII")); + headerStream.writeLong(ctrlBlockLen); // ctrlBlockLen (compressed) + headerStream.writeLong(diffBlockLen); // diffBlockLen (compressed) + headerStream.writeLong(newsize); + headerStream.flush(); + + Utils.copyBytesToStream(arrayOut, diffFileOut); + + return newBuf; + } + + private static class IntByRef { + public int value; + } +} + diff --git a/updater/src/ie/wombat/jbdiff/JBPatch.java b/updater/src/ie/wombat/jbdiff/JBPatch.java new file mode 100644 index 000000000000..37ef1dd013af --- /dev/null +++ b/updater/src/ie/wombat/jbdiff/JBPatch.java @@ -0,0 +1,152 @@ +/* +* Copyright (c) 2005, Joe Desbonnet, (jdesbonnet@gmail.com) +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * 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. +* * Neither the name of the <organization> nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY <copyright holder> ``AS IS'' AND ANY +* EXPRESS 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 <copyright holder> 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 ie.wombat.jbdiff; + +import com.intellij.updater.Utils; + +import java.io.*; +import java.util.zip.GZIPInputStream; + +/** + * Java Binary patcher (based on bspatch by Colin Percival) + * + * @author Joe Desbonnet, jdesbonnet@gmail.com + */ +public class JBPatch { + public static void bspatch(InputStream oldFileIn, OutputStream newFileOut, InputStream diffFileIn) + throws IOException { + + int oldpos, newpos; + + byte[] diffData = Utils.readBytes(diffFileIn); + + DataInputStream diffIn = new DataInputStream(new ByteArrayInputStream(diffData)); + + // headerMagic at header offset 0 (length 8 bytes) + long headerMagic = diffIn.readLong(); + + // ctrlBlockLen after gzip compression at heater offset 8 (length 8 bytes) + long ctrlBlockLen = diffIn.readLong(); + + // diffBlockLen after gzip compression at header offset 16 (length 8 bytes) + long diffBlockLen = diffIn.readLong(); + + // size of new file at header offset 24 (length 8 bytes) + int newsize = (int)diffIn.readLong(); + + /* + System.err.println ("newsize=" + newsize); + System.err.println ("ctrlBlockLen=" + ctrlBlockLen); + System.err.println ("diffBlockLen=" + diffBlockLen); + System.err.println ("newsize=" + newsize); + */ + + InputStream in; + in = new ByteArrayInputStream(diffData); + in.skip(ctrlBlockLen + 32); + GZIPInputStream diffBlockIn = new GZIPInputStream(in); + + in = new ByteArrayInputStream(diffData); + in.skip(diffBlockLen + ctrlBlockLen + 32); + GZIPInputStream extraBlockIn = new GZIPInputStream(in); + + /* + * Read in old file (file to be patched) to oldBuf + */ + ByteArrayOutputStream oldFileByteOut = new ByteArrayOutputStream(); + try { + Utils.copyStream(oldFileIn, oldFileByteOut); + } + finally { + oldFileByteOut.close(); + } + byte[] oldBuf = oldFileByteOut.toByteArray(); + int oldsize = oldBuf.length; + + byte[] newBuf = new byte[newsize + 1]; + + oldpos = 0; + newpos = 0; + int[] ctrl = new int[3]; + int nbytes; + while (newpos < newsize) { + + for (int i = 0; i <= 2; i++) { + ctrl[i] = diffIn.readInt(); + //System.err.println (" ctrl[" + i + "]=" + ctrl[i]); + } + + if (newpos + ctrl[0] > newsize) { + System.err.println("Corrupt patch\n"); + return; + } + + /* + * Read ctrl[0] bytes from diffBlock stream + */ + + if (!Util.readFromStream(diffBlockIn, newBuf, newpos, ctrl[0])) { + System.err.println("error reading from extraIn"); + return; + } + + for (int i = 0; i < ctrl[0]; i++) { + if ((oldpos + i >= 0) && (oldpos + i < oldsize)) { + newBuf[newpos + i] += oldBuf[oldpos + i]; + } + } + + newpos += ctrl[0]; + oldpos += ctrl[0]; + + if (newpos + ctrl[1] > newsize) { + System.err.println("Corrupt patch"); + return; + } + + if (!Util.readFromStream(extraBlockIn, newBuf, newpos, ctrl[1])) { + System.err.println("error reading from extraIn"); + return; + } + + newpos += ctrl[1]; + oldpos += ctrl[2]; + } + + // TODO: Check if at end of ctrlIn + // TODO: Check if at the end of diffIn + // TODO: Check if at the end of extraIn + + diffBlockIn.close(); + extraBlockIn.close(); + diffIn.close(); + + newFileOut.write(newBuf, 0, newBuf.length - 1); + } +} + diff --git a/updater/src/ie/wombat/jbdiff/Util.java b/updater/src/ie/wombat/jbdiff/Util.java new file mode 100644 index 000000000000..b5188976f8b0 --- /dev/null +++ b/updater/src/ie/wombat/jbdiff/Util.java @@ -0,0 +1,112 @@ +/* +* Copyright (c) 2005, Joe Desbonnet, (jdesbonnet@gmail.com) +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * 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. +* * Neither the name of the <organization> nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY <copyright holder> ``AS IS'' AND ANY +* EXPRESS 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 <copyright holder> 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 ie.wombat.jbdiff; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @author Joe Desbonnet, jdesbonnet@gmail.com + * + */ +public class Util { + + /** + * Equiv of C library memcmp(). + * + * @param s1 + * @param s1offset + * @param s2 + * @param n + * @return + */ + /* + public final static int memcmp(byte[] s1, int s1offset, byte[] s2, int s2offset, int n) { + + if ((s1offset + n) > s1.length) { + n = s1.length - s1offset; + } + if ((s2offset + n) > s2.length) { + n = s2.length - s2offset; + } + for (int i = 0; i < n; i++) { + if (s1[i + s1offset] != s2[i + s2offset]) { + return s1[i + s1offset] < s2[i + s2offset] ? -1 : 1; + } + } + + return 0; + } + */ + + /** + * Equiv of C library memcmp(). + * + * @param s1 + * @param s1offset + * @param s2 + * @param n + * @return + */ + public final static int memcmp(byte[] s1, int s1offset, byte[] s2, int s2offset) { + + int n = s1.length - s1offset; + + if (n > (s2.length-s2offset)) { + n = s2.length-s2offset; + } + for (int i = 0; i < n; i++) { + if (s1[i + s1offset] != s2[i + s2offset]) { + return s1[i + s1offset] < s2[i + s2offset] ? -1 : 1; + } + } + + return 0; + } + + public static final boolean readFromStream (InputStream in, byte[] buf, int offset, int len) + throws IOException + { + + int totalBytesRead = 0; + int nbytes; + + while ( totalBytesRead < len) { + nbytes = in.read(buf,offset+totalBytesRead,len-totalBytesRead); + if (nbytes < 0) { + System.err.println ("readFromStream(): returning prematurely. Read " + + totalBytesRead + " bytes"); + return false; + } + totalBytesRead+=nbytes; + } + + return true; + } + +} diff --git a/updater/updater.iml b/updater/updater.iml new file mode 100644 index 000000000000..c053808175a3 --- /dev/null +++ b/updater/updater.iml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module relativePaths="true" type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> + |