| /* |
| * 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.core.output2; |
| |
| import java.util.logging.Logger; |
| import org.openide.util.NbBundle; |
| |
| import java.io.*; |
| import java.lang.reflect.Method; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.FileChannel; |
| import java.text.MessageFormat; |
| import java.util.HashSet; |
| import java.util.Set; |
| import org.openide.util.Exceptions; |
| import org.openide.util.RequestProcessor; |
| |
| /** |
| * An implementation of the Storage interface over a memory mapped file. |
| * |
| */ |
| class FileMapStorage implements Storage { |
| /** A file channel for reading/writing the mapped file */ |
| private FileChannel fileChannel; |
| /** The base number of bytes to allocate when a getWriteBuffer for writing is |
| * needed. */ |
| private static final int BASE_BUFFER_SIZE = 8196; |
| /** |
| * max possible range to map.. 1 MB |
| */ |
| private static final long MAX_MAP_RANGE = 1024 * 1024; |
| /** |
| * Own request processor |
| */ |
| private static final RequestProcessor RP = new RequestProcessor("FileMapStorage"); //NOI18N |
| /** |
| * List of storages that have not been disposed yet. |
| */ |
| private static final Set<FileMapStorage> undisposed; |
| /** |
| * The byte getWriteBuffer that write operations write into. Actual buffers are |
| * provided for writing by calling master.slice(); this getWriteBuffer simply |
| * pre-allocates a fairly large chunk of memory to reduce repeated |
| * allocations. |
| */ |
| private ByteBuffer master; |
| /** A byte getWriteBuffer mapped to the contents of the output file, from which |
| * content is read. */ |
| private MappedBufferResource contents; |
| /** The number of bytes from the file that have been are currently mapped |
| * into the contents ByteBuffer. This will be checked on calls that read, |
| * and if more than the currently mapped bytes are requested, the |
| * contents bufffer will be replaced by a larger one */ |
| private long mappedRange; |
| |
| /** |
| * start of the mapped range.. |
| */ |
| private long mappedStart; |
| /** |
| * The currently in use buffer. |
| */ |
| private ByteBuffer buffer = null; |
| /** |
| * The number of bytes that have been written. |
| */ |
| protected int bytesWritten = 0; |
| /** |
| * The file we are writing to. |
| */ |
| private File outfile = null; |
| |
| private int outstandingBufferCount = 0; |
| |
| /** |
| * Byte in the file that corresponds to logical start of the storage data. |
| * Data before this offset are "forgotten". |
| */ |
| private long startOffset = 0; |
| |
| private boolean closed; |
| |
| static { |
| undisposed = new HashSet<FileMapStorage>(); |
| |
| // Remove all remaining temporary files before exit. |
| Runtime.getRuntime().addShutdownHook(new Thread() { |
| |
| @Override |
| public void run() { |
| for (FileMapStorage fms : undisposed) { |
| if (fms.contents != null) { |
| fms.contents.releaseBuffer(); |
| } |
| if (fms.fileChannel != null && fms.fileChannel.isOpen()) { |
| try { |
| fms.fileChannel.close(); |
| } catch (IOException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| if (fms.outfile != null) { |
| fms.outfile.delete(); |
| } |
| } |
| } |
| }); |
| } |
| |
| FileMapStorage() { |
| init(); |
| } |
| |
| private void init() { |
| contents = null; |
| mappedRange = -1; |
| mappedStart = 0; |
| master = ByteBuffer.allocateDirect (BASE_BUFFER_SIZE); |
| fileChannel = null; |
| buffer = null; |
| bytesWritten = 0; |
| closed = true; |
| addUndisposed(this); |
| } |
| |
| /** |
| * Ensure that the output file exists. |
| */ |
| private void ensureFileExists() throws IOException { |
| if (outfile == null) { |
| String outdir = System.getProperty("java.io.tmpdir"); //NOI18N |
| if (!outdir.endsWith(File.separator)) { |
| outdir += File.separator; |
| } |
| File dir = new File (outdir); |
| if (!dir.exists()) { |
| //Handle the event that we cannot find the system temporary directory |
| IllegalStateException ise = new IllegalStateException ("Cannot find temp directory " + outdir); //NOI18N |
| Exceptions.attachLocalizedMessage(ise, NbBundle.getMessage(OutWriter.class, "FMT_CannotWrite", outdir)); |
| throw ise; |
| } |
| //#47196 - if user holds down F9, many threads can enter this method |
| //simultaneously and all try to create the same file |
| synchronized (FileMapStorage.class) { |
| StringBuilder fname = new StringBuilder(outdir) |
| .append("output").append(Long.toString(System.currentTimeMillis())); //NOI18N |
| outfile = new File (fname.toString()); |
| while (outfile.exists()) { |
| fname.append('x'); //NOI18N |
| outfile = new File(fname.toString()); |
| } |
| outfile.createNewFile(); |
| if (!outfile.exists() || !outfile.canWrite()) { |
| //Handle the (unlikely) case we cannot write to the system temporary directory |
| IllegalStateException ise = new IllegalStateException ("Cannot write to " + fname); //NOI18N |
| Exceptions.attachLocalizedMessage(ise, NbBundle.getMessage(OutWriter.class, "FMT_CannotWrite", outdir)); |
| throw ise; |
| } |
| outfile.deleteOnExit(); |
| } |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return outfile == null ? "[unused or disposed FileMapStorage]" : outfile.getPath(); |
| } |
| |
| private FileChannel writeChannel() throws IOException { |
| FileChannel channel = fileChannel(); |
| closed = !channel.isOpen(); |
| return channel; |
| } |
| |
| /** |
| * Get a FileChannel opened for reading/writing against the output file. |
| */ |
| private FileChannel fileChannel() throws IOException { |
| if (fileChannel == null || !fileChannel.isOpen()) { |
| ensureFileExists(); |
| RandomAccessFile raf = new RandomAccessFile(outfile, "rw"); |
| fileChannel = raf.getChannel(); |
| } |
| return fileChannel; |
| } |
| |
| /** |
| * Fetch a getWriteBuffer of a specified size to use for appending new data to the |
| * end of the file. |
| */ |
| public synchronized ByteBuffer getWriteBuffer (int size) { |
| if (master.capacity() - master.position() < size) { |
| int newSize = Math.max (BASE_BUFFER_SIZE * 2, |
| size + BASE_BUFFER_SIZE); |
| |
| master = ByteBuffer.allocateDirect (newSize); |
| } |
| |
| if (buffer == null) { |
| buffer = master.slice(); |
| } else { |
| int charsRemaining = AbstractLines.toCharIndex(buffer.capacity() - buffer.position()); |
| |
| if (charsRemaining < size) { |
| buffer.flip(); |
| buffer = master.slice(); |
| } |
| } |
| outstandingBufferCount++; |
| return buffer; |
| } |
| |
| /** |
| * Dispose of a ByteBuffer which has been acquired for writing by one of |
| * the write methods, writing its contents to the file. |
| */ |
| public int write (ByteBuffer bb) throws IOException { |
| synchronized (this) { |
| if (bb == buffer) { |
| buffer = null; |
| } |
| } |
| int position = size(); |
| int byteCount = bb.position(); |
| bb.flip(); |
| FileChannel channel = writeChannel(); |
| if (channel.isOpen()) { //If a thread was terminated while writing, it will be closed |
| Thread.interrupted(); // #186629: must clear interrupt flag or channel will be broken |
| channel.write (bb); |
| synchronized (this) { |
| bytesWritten += byteCount; |
| outstandingBufferCount--; |
| } |
| } |
| return position; |
| } |
| |
| @Override |
| public synchronized void removeBytesFromEnd(int length) throws IOException { |
| if (length == 0) { |
| return; |
| } |
| FileChannel channel = writeChannel(); |
| channel.position(channel.position() - length); |
| bytesWritten -= length; |
| } |
| |
| public synchronized void dispose() { |
| if (Controller.LOG) { |
| Controller.log ("Disposing file map storage"); |
| Controller.logStack(); |
| } |
| final FileChannel oldChannel = fileChannel; |
| final File oldFile = outfile; |
| final MappedBufferResource oldContents = contents; |
| fileChannel = null; |
| closed = true; |
| outfile = null; |
| buffer = null; |
| contents = null; |
| |
| if (oldChannel != null || oldFile != null) { |
| RP.post(new Runnable() { |
| |
| public void run() { |
| try { |
| if (oldContents != null) { |
| oldContents.releaseBuffer(); |
| } |
| if (oldChannel != null && oldChannel.isOpen()) { |
| oldChannel.close(); |
| } |
| if (oldFile != null && oldFile.exists()) { |
| oldFile.delete(); |
| } |
| } catch (Exception e) { |
| Exceptions.printStackTrace(e); |
| } |
| } |
| }); |
| } |
| removeUndisposed(this); |
| } |
| |
| File getOutputFile() { |
| return outfile; |
| } |
| |
| /** |
| * Workaround for JDK issue #4715154 (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4715154) |
| */ |
| private static void unmap(Object buffer) { |
| try { |
| Method getCleanerMethod = buffer.getClass().getMethod("cleaner"); |
| getCleanerMethod.setAccessible(true); |
| /*sun.misc.Cleaner*/Object cleaner = getCleanerMethod.invoke(buffer); |
| if (cleaner != null) { |
| cleaner.getClass().getMethod("clean").invoke(cleaner); |
| } |
| } catch (Exception e) { |
| } |
| } |
| |
| /** |
| * Get a byte buffer representing the a getText of the contents of the |
| * output file. This is optimized to possibly map more of the output file |
| * into memory if it is not already mapped. |
| */ |
| public BufferResource<ByteBuffer> getReadBuffer(int start, int byteCount) |
| throws IOException { |
| |
| ByteBuffer cont; |
| long fileStart = startOffset + start; |
| synchronized (this) { |
| cont = this.contents == null ? null : this.contents.getBuffer(); |
| if (cont == null || fileStart + byteCount > mappedRange || fileStart < mappedStart) { |
| FileChannel ch = fileChannel(); |
| mappedStart = Math.max((long)0, fileStart - (MAX_MAP_RANGE /2)); |
| long prevMappedRange = mappedRange; |
| long map = byteCount > (MAX_MAP_RANGE / 2) ? (byteCount + byteCount / 10) : (MAX_MAP_RANGE / 2); |
| mappedRange = Math.min(ch.size(), fileStart + map); |
| try { |
| try { |
| cont = ch.map(FileChannel.MapMode.READ_ONLY, mappedStart, mappedRange - mappedStart); |
| updateContents(cont); |
| } catch (IOException ioe) { |
| Logger.getAnonymousLogger().info("Failed to memory map output file for reading. Trying to read it normally."); //NOI18N |
| |
| // Memory mapping failed, fallback to non-mapped |
| cont = ByteBuffer.allocate((int) (mappedRange - mappedStart)); |
| ch.read(cont, mappedStart); |
| updateContents(cont); |
| } |
| } catch (Exception e) { |
| String msg = MessageFormat.format( |
| "Failed to read output file. Start:{0} bytes reqd={1}" //NOI18N |
| + " mapped range={2} previous mapped range={3} " //NOI18N |
| + "channel size: {4}", new Object[]{start, byteCount, |
| mappedRange, prevMappedRange, ch.size()}); |
| throw new IOException(msg, e); |
| } |
| } |
| if (fileStart - mappedStart > cont.limit() - byteCount) { |
| cont.position(Math.max(0, cont.limit() - byteCount)); |
| } else { |
| cont.position((int) (fileStart - mappedStart)); |
| } |
| } |
| int limit = Math.min(cont.limit(), byteCount); |
| try { |
| return new ChildBufferResource((ByteBuffer)cont.slice().limit(limit), this.contents); |
| } catch (Exception e) { |
| throw new IllegalStateException ("Error setting limit to " + limit //NOI18N |
| + " contents size = " + cont.limit() + " requested: read " + //NOI18N |
| "buffer from " + start + " to be " + byteCount + " bytes"); //NOI18N |
| } |
| } |
| |
| private void updateContents(ByteBuffer buffer) { |
| if (this.contents != null) { |
| this.contents.decRefs(); |
| } |
| this.contents = new MappedBufferResource(buffer); |
| this.contents.incRefs(); |
| } |
| |
| public synchronized int size() { |
| return bytesWritten; |
| } |
| |
| public void flush() throws IOException { |
| if (buffer != null) { |
| if (Controller.LOG) Controller.log("FILEMAP STORAGE flush(): " + outstandingBufferCount); |
| write (buffer); |
| fileChannel.force(false); |
| buffer = null; |
| } |
| } |
| |
| public void close() throws IOException { |
| if (fileChannel != null) { |
| flush(); |
| } |
| closed = true; |
| } |
| |
| public boolean isClosed() { |
| return fileChannel == null || closed; |
| } |
| |
| private static synchronized void addUndisposed(FileMapStorage fms) { |
| undisposed.add(fms); |
| } |
| |
| private static synchronized void removeUndisposed(FileMapStorage fms) { |
| undisposed.remove(fms); |
| } |
| |
| @Override |
| public void shiftStart(int byteOffset) { |
| synchronized (this) { |
| startOffset += byteOffset; |
| bytesWritten -= byteOffset; |
| } |
| } |
| |
| private class ChildBufferResource implements BufferResource<ByteBuffer> { |
| |
| private ByteBuffer buffer; |
| private MappedBufferResource parentResource; |
| |
| public ChildBufferResource( |
| ByteBuffer buffer, MappedBufferResource parentResource) { |
| |
| this.buffer = buffer; |
| this.parentResource = parentResource; |
| this.parentResource.incRefs(); |
| } |
| |
| @Override |
| public ByteBuffer getBuffer() { |
| return buffer; |
| } |
| |
| @Override |
| public void releaseBuffer() { |
| buffer = null; |
| parentResource.decRefs(); |
| } |
| } |
| |
| private class MappedBufferResource |
| implements BufferResource<ByteBuffer> { |
| |
| private int refs = 0; |
| private ByteBuffer buffer; |
| |
| public MappedBufferResource(ByteBuffer buffer) { |
| this.buffer = buffer; |
| } |
| |
| @Override |
| public void releaseBuffer() { |
| if (buffer != null) { |
| unmap(buffer); |
| buffer = null; |
| } |
| } |
| |
| @Override |
| public ByteBuffer getBuffer() { |
| return buffer; |
| } |
| |
| synchronized void incRefs() { |
| refs++; |
| } |
| |
| synchronized void decRefs() { |
| refs--; |
| assert refs >= 0; |
| if (refs == 0) { |
| unmap(buffer); |
| buffer = null; |
| } |
| } |
| } |
| } |