blob: a380e96507183a7ca01890289659555c1f77e219 [file] [log] [blame]
/*
* 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;
}
}
}
}