| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.netbeans; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.ByteArrayInputStream; |
| import java.io.DataInput; |
| import java.io.DataInputStream; |
| import java.io.DataOutput; |
| import java.io.DataOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.MappedByteBuffer; |
| import java.nio.channels.FileChannel; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Random; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipInputStream; |
| import org.openide.modules.Places; |
| import org.openide.util.Exceptions; |
| import org.openide.util.NbBundle; |
| |
| /** |
| * Support for optimal checking of time stamps of certain files in |
| * NetBeans directory structure. |
| * |
| * @author Jaroslav Tulach <jaroslav.tulach@netbeans.org> |
| * @since 2.9 |
| */ |
| public final class Stamps { |
| private static final Logger LOG = Logger.getLogger(Stamps.class.getName()); |
| private static AtomicLong moduleJARs; |
| private static File moduleNewestFile; |
| private static File[] fallbackCache; |
| private static boolean populated; |
| private static Boolean clustersChanged; |
| |
| private Worker worker = new Worker(); |
| |
| private Stamps() { |
| } |
| |
| /** This class can be executed from command line to perform various checks |
| * on installed NetBeans, however outside of running NetBeans. |
| * |
| */ |
| static void main(String... args) { |
| if (args.length == 1 && "reset".equals(args[0])) { // NOI18N |
| moduleJARs = null; |
| Clusters.clear(); |
| clustersChanged = null; |
| fallbackCache = null; |
| stamp(false); |
| return; |
| } |
| if (args.length == 1 && "init".equals(args[0])) { // NOI18N |
| moduleJARs = null; |
| Clusters.clear(); |
| clustersChanged = null; |
| fallbackCache = null; |
| stamp(true); |
| return; |
| } |
| if (args.length == 1 && "clear".equals(args[0])) { // NOI18N |
| moduleJARs = null; |
| Clusters.clear(); |
| clustersChanged = null; |
| fallbackCache = null; |
| return; |
| } |
| } |
| private static final Stamps MODULES_JARS = new Stamps(); |
| /** Creates instance of stamp that checks timestamp for all files that affect |
| * module classloading and related caches. |
| */ |
| public static Stamps getModulesJARs() { |
| return MODULES_JARS; |
| } |
| |
| /** Finds out the time of last modifications of files that influnce |
| * this cache. Each cached file needs to be "younger". |
| * @return time in ms since epoch |
| */ |
| public long lastModified() { |
| return moduleJARs(); |
| } |
| |
| /** Checks whether a cache exists |
| * |
| * @param cache name of the cache |
| * @return true if the cache exists and is not out of date |
| */ |
| public boolean exists(String cache) { |
| return file(cache, null) != null; |
| } |
| |
| /** Opens the access to cache object as a stream. |
| * @param name name of the cache |
| * @return stream to read from the cache or null if the cache is not valid |
| */ |
| public InputStream asStream(String cache) { |
| ByteBuffer bb = asByteBuffer(cache, false, false); |
| if (bb == null) { |
| return null; |
| } |
| return new ByteArrayInputStream(bb.array()); |
| } |
| |
| /** Getter for mmapped buffer access to the cache. |
| * @param cache the file to access |
| * @return mmapped read only buffer |
| */ |
| public MappedByteBuffer asMappedByteBuffer(String cache) { |
| return (MappedByteBuffer)asByteBuffer(cache, true, true); |
| } |
| |
| /** Returns the stamp for this caches. |
| * @return a date, each cache needs to be newer than this date |
| */ |
| |
| /** Opens the access to cache object as a stream. |
| * @param name name of the cache |
| * @return stream to read from the cache or null if the cache is not valid |
| */ |
| public ByteBuffer asByteBuffer(String cache) { |
| return asByteBuffer(cache, true, false); |
| } |
| final File file(String cache, int[] len) { |
| if (clustersChanged()) { |
| return null; |
| } |
| |
| checkPopulateCache(); |
| |
| synchronized (this) { |
| if (worker.isProcessing(cache)) { |
| LOG.log(Level.FINE, "Worker processing when asking for {0}", cache); // NOI18N |
| return null; |
| } |
| } |
| return fileImpl(cache, len, moduleJARs()); |
| } |
| |
| private ByteBuffer asByteBuffer(String cache, boolean direct, boolean mmap) { |
| int[] len = new int[1]; |
| File cacheFile = file(cache, len); |
| if (cacheFile == null) { |
| return null; |
| } |
| |
| try { |
| FileChannel fc = new FileInputStream(cacheFile).getChannel(); |
| ByteBuffer master; |
| if (mmap) { |
| master = fc.map(FileChannel.MapMode.READ_ONLY, 0, len[0]); |
| master.order(ByteOrder.LITTLE_ENDIAN); |
| } else { |
| master = direct ? ByteBuffer.allocateDirect(len[0]) : ByteBuffer.allocate(len[0]); |
| int red = fc.read(master); |
| if (red != len[0]) { |
| LOG.warning("Read less than expected: " + red + " expected: " + len + " for " + cacheFile); // NOI18N |
| return null; |
| } |
| master.flip(); |
| } |
| |
| fc.close(); |
| |
| return master; |
| } catch (IOException ex) { |
| LOG.log(Level.WARNING, "Cannot read cache " + cacheFile, ex); // NOI18N |
| return null; |
| } |
| } |
| |
| /** Method for registering updates to caches. |
| * @param updater the callback to start when flushing caches |
| * @param file name of the file to store the cache into |
| * @param append write from scratch or append? |
| */ |
| public void scheduleSave(Updater updater, String cache, boolean append) { |
| boolean firstAdd; |
| firstAdd = scheduleSaveImpl(updater, cache, append); |
| LOG.log(firstAdd ? Level.FINE : Level.FINER, |
| "Scheduling save for {0} cache", cache |
| ); |
| Clusters.scheduleSave(this); |
| } |
| |
| final boolean scheduleSaveImpl(Updater updater, String cache, boolean append) { |
| synchronized (worker) { |
| return worker.addStorage(new Store(updater, cache, append)); |
| } |
| } |
| |
| /** Flushes all caches. |
| * @param delay the delay to wait with starting the parsing, if zero, that also means |
| * we want to wait for the end of parsing |
| */ |
| public void flush(int delay) { |
| synchronized (worker) { |
| worker.start(delay); |
| } |
| } |
| |
| /** Waits for the worker to finish */ |
| public void shutdown() { |
| waitFor(true); |
| } |
| |
| public void discardCaches() { |
| discardCachesImpl(moduleJARs); |
| } |
| |
| private static void discardCachesImpl(AtomicLong al) { |
| File user = Places.getUserDirectory(); |
| long now = System.currentTimeMillis(); |
| if (user != null) { |
| File f = new File(user, ".lastModified"); |
| if (f.exists()) { |
| f.setLastModified(now); |
| } else { |
| f.getParentFile().mkdirs(); |
| try { |
| f.createNewFile(); |
| } catch (IOException ex) { |
| LOG.log(Level.WARNING, "Cannot create " + f, ex); |
| } |
| } |
| } |
| if (al != null) { |
| al.set(now); |
| } |
| } |
| |
| final void waitFor(boolean noNotify) { |
| Worker wait; |
| synchronized (worker) { |
| flush(0); |
| wait = worker; |
| } |
| wait.waitFor(noNotify); |
| } |
| |
| |
| /** Computes and returns timestamp for all files that affect |
| * module classloading and related caches. |
| * @return |
| */ |
| static long moduleJARs() { |
| AtomicLong local = moduleJARs; |
| if (local == null) { |
| local = new AtomicLong(); |
| AtomicReference<File> newestFile = new AtomicReference<File>(); |
| stamp(true, local, newestFile); |
| moduleJARs = local; |
| moduleNewestFile = newestFile.get(); |
| } |
| return local.longValue(); |
| } |
| |
| // |
| // Implementation. As less dependecies on other NetBeans clases, as possible, please. |
| // This will be called externally from a launcher. |
| // |
| |
| private static AtomicLong stamp(boolean checkStampFile) { |
| AtomicLong result = new AtomicLong(); |
| AtomicReference<File> newestFile = new AtomicReference<File>(); |
| stamp(checkStampFile, result, newestFile); |
| return result; |
| } |
| |
| private static void stamp(boolean checkStampFile, AtomicLong result, AtomicReference<File> newestFile) { |
| StringBuilder sb = new StringBuilder(); |
| |
| Set<File> processedDirs = new HashSet<File>(); |
| String[] relativeDirs = Clusters.relativeDirsWithHome(); |
| String home = System.getProperty ("netbeans.home"); // NOI18N |
| if (home != null) { |
| long stamp = stampForCluster (new File (home), result, newestFile, processedDirs, checkStampFile, true, null); |
| sb.append(relativeDirs[0]).append('=').append(stamp).append('\n'); |
| } |
| String[] drs = Clusters.dirs(); |
| for (int i = 0; i < drs.length; i++) { |
| final File clusterDir = new File(drs[i]); |
| long stamp = stampForCluster(clusterDir, result, newestFile, processedDirs, checkStampFile, true, null); |
| if (stamp != -1) { |
| sb.append("cluster.").append(relativeDirs[i + 1]).append('=').append(stamp).append('\n'); |
| } |
| } |
| File user = Places.getUserDirectory(); |
| if (user != null) { |
| AtomicInteger crc = new AtomicInteger(); |
| stampForCluster(user, result, newestFile, new HashSet<File>(), false, false, crc); |
| sb.append("user=").append(result.longValue()).append('\n'); |
| sb.append("crc=").append(crc.intValue()).append('\n'); |
| sb.append("locale=").append(Locale.getDefault()).append('\n'); |
| sb.append("branding=").append(NbBundle.getBranding()).append('\n'); |
| sb.append("java.version=").append(System.getProperty("java.version")).append('\n'); |
| sb.append("java.vm.version=").append(System.getProperty("java.vm.version")).append('\n'); |
| |
| File checkSum = new File(Places.getCacheDirectory(), "lastModified/all-checksum.txt"); |
| if (!compareAndUpdateFile(checkSum, sb.toString(), result)) { |
| discardCachesImpl(result); |
| } |
| } |
| } |
| |
| private static long stampForCluster( |
| File cluster, AtomicLong result, AtomicReference<File> newestFile, Set<File> hashSet, |
| boolean checkStampFile, boolean createStampFile, AtomicInteger crc |
| ) { |
| File stamp = new File(cluster, ".lastModified"); // NOI18N |
| long time; |
| if (checkStampFile && (time = stamp.lastModified()) > 0) { |
| if (time > result.longValue()) { |
| newestFile.set(stamp); |
| result.set(time); |
| } |
| return time; |
| } |
| if (Places.getUserDirectory() != null) { |
| stamp = new File(new File(Places.getCacheDirectory(), "lastModified"), clusterLocalStamp(cluster)); |
| if (checkStampFile && (time = stamp.lastModified()) > 0) { |
| if (time > result.longValue()) { |
| newestFile.set(stamp); |
| result.set(time); |
| } |
| return time; |
| } |
| } else { |
| createStampFile = false; |
| } |
| |
| File configDir = new File(new File(cluster, "config"), "Modules"); // NOI18N |
| File modulesDir = new File(cluster, "modules"); // NOI18N |
| |
| AtomicLong clusterResult = new AtomicLong(); |
| AtomicReference<File> newestInCluster = new AtomicReference<File>(); |
| if (highestStampForDir(configDir, newestInCluster, clusterResult, crc) && highestStampForDir(modulesDir, newestInCluster, clusterResult, crc)) { |
| // ok |
| } else { |
| if (!cluster.isDirectory()) { |
| // skip non-existing clusters` |
| return -1; |
| } |
| } |
| |
| if (clusterResult.longValue() > result.longValue()) { |
| newestFile.set(newestInCluster.get()); |
| result.set(clusterResult.longValue()); |
| } |
| |
| if (createStampFile) { |
| try { |
| stamp.getParentFile().mkdirs(); |
| stamp.createNewFile(); |
| stamp.setLastModified(clusterResult.longValue()); |
| } catch (IOException ex) { |
| System.err.println("Cannot write timestamp to " + stamp); // NOI18N |
| } |
| } |
| return clusterResult.longValue(); |
| } |
| |
| private static boolean highestStampForDir(File file, AtomicReference<File> newestFile, AtomicLong result, AtomicInteger crc) { |
| if (file.getName().equals(".nbattrs")) { // NOI18N |
| return true; |
| } |
| |
| File[] children = file.listFiles(); |
| if (children == null) { |
| if (crc != null) { |
| crc.addAndGet(file.getName().length()); |
| } |
| long time = file.lastModified(); |
| if (time > result.longValue()) { |
| newestFile.set(file); |
| result.set(time); |
| } |
| return false; |
| } |
| |
| for (File f : children) { |
| highestStampForDir(f, newestFile, result, crc); |
| } |
| return true; |
| } |
| |
| private static boolean compareAndUpdateFile(File file, String content, AtomicLong result) { |
| try { |
| byte[] expected = content.getBytes("UTF-8"); // NOI18N |
| byte[] read = new byte[expected.length]; |
| FileInputStream is = null; |
| boolean areCachesOK; |
| boolean writeFile; |
| long lastMod; |
| try { |
| is = new FileInputStream(file); |
| int len = is.read(read); |
| areCachesOK = len == read.length && is.available() == 0 && Arrays.equals(expected, read); |
| writeFile = !areCachesOK; |
| lastMod = file.lastModified(); |
| } catch (FileNotFoundException notFoundEx) { |
| // ok, running for the first time, no need to invalidate the cache |
| areCachesOK = true; |
| writeFile = true; |
| lastMod = result.get(); |
| } finally { |
| if (is != null) { |
| is.close(); |
| } |
| } |
| if (writeFile) { |
| file.getParentFile().mkdirs(); |
| FileOutputStream os = new FileOutputStream(file); |
| os.write(expected); |
| os.close(); |
| if (areCachesOK) { |
| file.setLastModified(lastMod); |
| } |
| } else { |
| if (lastMod > result.get()) { |
| result.set(lastMod); |
| } |
| } |
| return areCachesOK; |
| } catch (IOException ex) { |
| ex.printStackTrace(); |
| return false; |
| } |
| } |
| |
| private static void deleteCache(File cacheFile) throws IOException { |
| int fileCounter = 0; |
| if (cacheFile.exists()) { |
| // all of this mess is here because Windows can't delete mmaped file. |
| File tmpFile = new File(cacheFile.getParentFile(), cacheFile.getName() + "." + fileCounter++); |
| tmpFile.delete(); // delete any leftover file from previous session |
| boolean renamed = false; |
| Random r = null; |
| for (int i = 0; i < 10; i++) { |
| renamed = cacheFile.renameTo(tmpFile); // try to rename it |
| if (renamed) { |
| break; |
| } |
| LOG.log(Level.INFO, "cannot rename (#{0}): {1}", new Object[]{i, cacheFile}); // NOI18N |
| // try harder |
| System.gc(); |
| System.runFinalization(); |
| LOG.info("after GC"); // NOI18N |
| if (r == null) { |
| r = new Random(); |
| } |
| try { |
| final int ms = r.nextInt(1000) + 1; |
| Thread.sleep(ms); |
| LOG.log(Level.INFO, "Slept {0} ms", ms); |
| } catch (InterruptedException ex) { |
| LOG.log(Level.INFO, "Interrupted", ex); // NOI18N |
| } |
| } |
| if (!renamed) { |
| // still delete on exit, so next start is ok |
| cacheFile.deleteOnExit(); |
| throw new IOException("Could not delete: " + cacheFile); // NOI18N |
| } |
| if (!tmpFile.delete()) { |
| tmpFile.deleteOnExit(); |
| } // delete now or later |
| } |
| } |
| |
| private static File findFallbackCache(String cache) { |
| String fallbackCacheLocation = System.getProperty("netbeans.fallback.cache"); // NOI18N |
| if ("none".equals(fallbackCacheLocation)) { // NOI18N |
| return null; |
| } |
| |
| if (fallbackCache == null) { |
| fallbackCache = new File[0]; |
| if (fallbackCacheLocation != null) { |
| File fallbackFile = new File(fallbackCacheLocation); |
| if (fallbackFile.isDirectory()) { |
| fallbackCache = new File[]{fallbackFile}; |
| } |
| } |
| if (fallbackCache.length == 0 && Clusters.dirs().length >= 1) { |
| File fallback = new File(new File(new File(Clusters.dirs()[0]), "var"), "cache"); // NOI18N |
| if (fallback.isDirectory()) { |
| fallbackCache = new File[]{ fallback }; |
| } |
| } |
| } |
| if (fallbackCache.length == 0) { |
| return null; |
| } |
| return new File(fallbackCache[0], cache); |
| } |
| |
| static void checkPopulateCache() { |
| if (populated) { |
| return; |
| } |
| populated = true; |
| |
| File cache = Places.getCacheDirectory(); |
| String[] children = cache.list(); |
| if (children != null && children.length > 0) { |
| return; |
| } |
| InputStream is = Stamps.getModulesJARs().asStream("populate.zip"); // NOI18N |
| if (is == null) { |
| return; |
| } |
| ZipInputStream zip = null; |
| FileOutputStream os = null; |
| try { |
| byte[] arr = new byte[4096]; |
| LOG.log(Level.FINE, "Found populate.zip about to extract it into {0}", cache); |
| zip = new ZipInputStream(is); |
| for (;;) { |
| ZipEntry en = zip.getNextEntry(); |
| if (en == null) { |
| break; |
| } |
| if (en.isDirectory()) { |
| continue; |
| } |
| File f = new File(cache, en.getName().replace('/', File.separatorChar)); |
| f.getParentFile().mkdirs(); |
| os = new FileOutputStream(f); |
| for (;;) { |
| int len = zip.read(arr); |
| if (len == -1) { |
| break; |
| } |
| os.write(arr, 0, len); |
| } |
| os.close(); |
| } |
| zip.close(); |
| } catch (IOException ex) { |
| LOG.log(Level.INFO, "Failed to populate {0}", cache); |
| } |
| } |
| |
| private static boolean clustersChanged() { |
| if (clustersChanged != null) { |
| return clustersChanged; |
| } |
| |
| final String clustersCache = "all-clusters.dat"; // NOI18N |
| File f = fileImpl(clustersCache, null, -1); // no timestamp check |
| if (f != null) { |
| DataInputStream dis = null; |
| try { |
| dis = new DataInputStream(new FileInputStream(f)); |
| if (Clusters.compareDirs(dis)) { |
| return false; |
| } |
| } catch (IOException ex) { |
| return clustersChanged = true; |
| } finally { |
| if (dis != null) { |
| try { |
| dis.close(); |
| } catch (IOException ex) { |
| LOG.log(Level.INFO, null, ex); |
| } |
| } |
| } |
| } else { |
| // missing cluster file signals caches are OK, for |
| // backward compatibility |
| return clustersChanged = false; |
| } |
| return clustersChanged = true; |
| } |
| |
| private static File fileImpl(String cache, int[] len, long moduleJARs) { |
| File cacheFile = new File(Places.getCacheDirectory(), cache); |
| long last = cacheFile.lastModified(); |
| if (last <= 0) { |
| LOG.log(Level.FINE, "Cache does not exist when asking for {0}", cache); // NOI18N |
| cacheFile = findFallbackCache(cache); |
| if (cacheFile == null || (last = cacheFile.lastModified()) <= 0) { |
| return null; |
| } |
| LOG.log(Level.FINE, "Found fallback cache at {0}", cacheFile); |
| } |
| |
| if (moduleJARs > last) { |
| LOG.log(Level.FINE, "Timestamp does not pass when asking for {0}. Newest file {1}", new Object[] { cache, moduleNewestFile }); // NOI18N |
| return null; |
| } |
| |
| long longLen = cacheFile.length(); |
| if (longLen > Integer.MAX_VALUE) { |
| LOG.log(Level.WARNING, "Cache file is too big: {0} bytes for {1}", new Object[]{longLen, cacheFile}); // NOI18N |
| return null; |
| } |
| if (len != null) { |
| len[0] = (int)longLen; |
| } |
| |
| LOG.log(Level.FINE, "Cache found: {0}", cache); // NOI18N |
| return cacheFile; |
| } |
| |
| /** A callback interface to flush content of some cache at a suitable |
| * point in time. |
| */ |
| public static interface Updater { |
| /** Callback method to allow storage of the cache to a stream. |
| * If an excetion is thrown, cache is invalidated. |
| * |
| * @param os the stream to write to |
| * @throws IOException exception in case something goes wrong |
| */ |
| public void flushCaches(DataOutputStream os) throws IOException; |
| |
| /** Callback method to notify the caller, that |
| * caches are successfully written. |
| */ |
| public void cacheReady(); |
| } |
| |
| /** Internal structure keeping info about storages. |
| */ |
| private static final class Store extends OutputStream { |
| final Updater updater; |
| final String cache; |
| final boolean append; |
| |
| OutputStream os; |
| AtomicInteger delay; |
| int count; |
| |
| public Store(Updater updater, String cache, boolean append) { |
| this.updater = updater; |
| this.cache = cache; |
| this.append = append; |
| } |
| |
| public boolean store(AtomicInteger delay) { |
| assert os == null; |
| |
| File cacheDir = Places.getCacheDirectory(); |
| if (!cacheDir.isDirectory()) { |
| LOG.log(Level.WARNING, "Nonexistent cache directory: {0}", cacheDir); // NOI18N |
| return false; |
| } |
| File cacheFile = new File(cacheDir, cache); // NOI18N |
| boolean delete = false; |
| try { |
| LOG.log(Level.FINE, "Cleaning cache {0}", cacheFile); |
| |
| if (!append) { |
| deleteCache(cacheFile); |
| } |
| cacheFile.getParentFile().mkdirs(); |
| |
| LOG.log(Level.FINE, "Storing cache {0}", cacheFile); |
| os = new FileOutputStream(cacheFile, append); //append new entries only |
| DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(this, 1024 * 1024)); |
| |
| this.delay = delay; |
| |
| updater.flushCaches(dos); |
| dos.close(); |
| LOG.log(Level.FINE, "Done Storing cache {0}", cacheFile); |
| } catch (IOException ex) { |
| LOG.log(Level.WARNING, "Error saving cache {0}", cacheFile); |
| LOG.log(Level.INFO, ex.getMessage(), ex); // NOI18N |
| delete = true; |
| } finally { |
| if (os != null) { |
| try { |
| os.close(); |
| } catch (IOException ex) { |
| LOG.log(Level.WARNING, "Error closing stream for " + cacheFile, ex); // NOI18N |
| } |
| os = null; |
| } |
| if (delete) { |
| cacheFile.delete(); |
| cacheFile.deleteOnExit(); |
| } else { |
| cacheFile.setLastModified(moduleJARs()); |
| } |
| } |
| return !delete; |
| } |
| |
| @Override |
| public void close() throws IOException { |
| os.close(); |
| } |
| |
| @Override |
| public void flush() throws IOException { |
| os.flush(); |
| } |
| |
| @Override |
| public void write(int b) throws IOException { |
| os.write(b); |
| count(1); |
| } |
| |
| @Override |
| public void write(byte[] b) throws IOException { |
| os.write(b); |
| count(b.length); |
| } |
| |
| @Override |
| public void write(byte[] b, int off, int len) throws IOException { |
| os.write(b, off, len); |
| count(len); |
| } |
| |
| private void count(int add) { |
| count += add; |
| if (count > 64 * 1024) { |
| int wait = delay.get(); |
| if (wait > 0) { |
| try { |
| Thread.sleep(wait); |
| } catch (InterruptedException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| count = 0; |
| } |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (obj == null) { |
| return false; |
| } |
| if (getClass() != obj.getClass()) { |
| return false; |
| } |
| final Store other = (Store) obj; |
| if (!this.updater.equals(other.updater)) { |
| return false; |
| } |
| if (!this.cache.equals(other.cache)) { |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| public int hashCode() { |
| int hash = 7; |
| hash = 19 * hash + (this.updater != null ? this.updater.hashCode() : 0); |
| hash = 19 * hash + (this.cache != null ? this.cache.hashCode() : 0); |
| return hash; |
| } |
| |
| @Override |
| public String toString() { |
| return cache; |
| } |
| } // end of Store |
| |
| private final class Worker extends Thread { |
| private final LinkedList<Store> storages; |
| private final HashSet<String> processing; |
| private AtomicInteger delay; |
| private boolean noNotify; |
| |
| public Worker() { |
| super("Flushing caches"); |
| storages = new LinkedList<Stamps.Store>(); |
| processing = new HashSet<String>(); |
| setPriority(MIN_PRIORITY); |
| } |
| |
| public synchronized void start(int time) { |
| if (delay == null) { |
| delay = new AtomicInteger(time); |
| super.start(); |
| } |
| } |
| |
| public synchronized boolean addStorage(Store s) { |
| boolean addNew = true; |
| processing.add(s.cache); |
| for (Iterator<Stamps.Store> it = storages.iterator(); it.hasNext();) { |
| Stamps.Store store = it.next(); |
| if (store.equals(s)) { |
| it.remove(); |
| addNew = false; |
| } |
| } |
| storages.add(s); |
| return addNew; |
| } |
| |
| @Override |
| public void run() { |
| int before = delay.get(); |
| for (int till = before; till >= 0; till -= 500) { |
| try { |
| synchronized (this) { |
| wait(500); |
| } |
| } catch (InterruptedException ex) { |
| LOG.log(Level.INFO, null, ex); |
| } |
| if (before != delay.get()) { |
| break; |
| } |
| } |
| if (before > 512) { |
| delay.compareAndSet(before, 512); |
| } |
| |
| long time = System.currentTimeMillis(); |
| LOG.log(Level.FINE, "Storing caches {0}", storages); |
| |
| HashSet<Store> notify = new HashSet<Stamps.Store>(); |
| for (;;) { |
| Store store; |
| synchronized (this) { |
| store = this.storages.poll(); |
| if (store == null) { |
| // ready for new round of work |
| worker = new Worker(); |
| break; |
| } |
| } |
| if (store.store(delay)) { |
| notify.add(store); |
| } |
| } |
| |
| long much = System.currentTimeMillis() - time; |
| LOG.log(Level.FINE, "Done storing caches {0}", notify); |
| LOG.log(Level.FINE, "Took {0} ms", much); |
| |
| processing.clear(); |
| |
| for (Stamps.Store store : notify) { |
| if (!noNotify) { |
| store.updater.cacheReady(); |
| } |
| } |
| LOG.log(Level.FINE, "Notified ready {0}", notify); |
| |
| } |
| |
| |
| final void waitFor(boolean noNotify) { |
| try { |
| this.noNotify = noNotify; |
| delay.set(0); |
| synchronized (this) { |
| notifyAll(); |
| } |
| join(); |
| } catch (InterruptedException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| |
| private boolean isProcessing(String cache) { |
| return processing.contains(cache); |
| } |
| |
| } |
| |
| static String clusterLocalStamp(File cluster) { |
| return cluster.getName().replaceAll("\\.\\.", "__"); |
| } |
| |
| static String readRelativePath(DataInput dis) throws IOException { |
| String index = dis.readUTF(); |
| if (index.isEmpty()) { |
| return index; |
| } |
| String relative = dis.readUTF(); |
| if ("user".equals(index)) { // NOI18N |
| return System.getProperty("netbeans.user").concat(relative); // NOI18N |
| } |
| if ("home".equals(index)) { // NOI18N |
| return System.getProperty("netbeans.home").concat(relative); // NOI18N |
| } |
| if ("abs".equals(index)) { // NOI18N |
| return relative; |
| } |
| int indx = Integer.parseInt(index); |
| String[] _dirs = Clusters.dirs(); |
| if (indx < 0 || indx >= _dirs.length) { |
| throw new IOException("Bad index " + indx + " for " + Arrays.toString(_dirs)); |
| } |
| return _dirs[indx].concat(relative); // NOI18N |
| } |
| |
| static void writeRelativePath(String path, DataOutput dos) throws IOException { |
| produceRelativePath(path, dos); |
| } |
| |
| private static void produceRelativePath(String path, Object out) throws IOException { |
| if (path.isEmpty()) { |
| if (out instanceof DataOutput) { |
| DataOutput dos = (DataOutput)out; |
| dos.writeUTF(path); |
| } |
| return; |
| } |
| if (testWritePath(path, System.getProperty("netbeans.user"), "user", out)) { // NOI18N |
| return; |
| } |
| int cnt = 0; |
| for (String p : Clusters.dirs()) { |
| if (testWritePath(path, p, "" + cnt, out)) { |
| return; |
| } |
| cnt++; |
| } |
| if (testWritePath(path, System.getProperty("netbeans.home"), "home", out)) { // NOI18N |
| return; |
| } |
| LOG.log(Level.FINE, "Cannot find relative path for {0}", path); // NOI18N |
| doWritePath("abs", path, out); // NOI18N |
| } |
| |
| private static boolean testWritePath(String path, String prefix, String codeName, Object out) throws IOException { |
| if (prefix == null || prefix.isEmpty()) { |
| return false; |
| } |
| if (path.startsWith(prefix)) { |
| final String relPath = path.substring(prefix.length()); |
| doWritePath(codeName, relPath, out); |
| return true; |
| } |
| return false; |
| } |
| private static void doWritePath(String codeName, String relPath, Object out) throws IOException { |
| if (out instanceof DataOutput) { |
| DataOutput dos = (DataOutput) out; |
| dos.writeUTF(codeName); |
| dos.writeUTF(relPath); |
| } else { |
| @SuppressWarnings("unchecked") |
| Collection<String> coll = (Collection<String>) out; |
| coll.add(codeName); |
| coll.add(relPath); |
| } |
| } |
| |
| static String findRelativePath(String file) { |
| List<String> arrayList = new ArrayList<String>(); |
| try { |
| produceRelativePath(file, arrayList); |
| } catch (IOException ex) { |
| return file; |
| } |
| return arrayList.get(1); |
| } |
| |
| |
| } |