|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
package java.util.logging; |
|
|
|
import static java.nio.file.StandardOpenOption.APPEND; |
|
import static java.nio.file.StandardOpenOption.CREATE_NEW; |
|
import static java.nio.file.StandardOpenOption.WRITE; |
|
|
|
import java.io.BufferedOutputStream; |
|
import java.io.File; |
|
import java.io.FileOutputStream; |
|
import java.io.IOException; |
|
import java.io.OutputStream; |
|
import java.nio.channels.FileChannel; |
|
import java.nio.channels.OverlappingFileLockException; |
|
import java.nio.file.FileAlreadyExistsException; |
|
import java.nio.file.Files; |
|
import java.nio.file.LinkOption; |
|
import java.nio.file.NoSuchFileException; |
|
import java.nio.file.Path; |
|
import java.nio.file.Paths; |
|
import java.security.AccessController; |
|
import java.security.PrivilegedAction; |
|
import java.util.HashSet; |
|
import java.util.Set; |
|
|
|
/** |
|
* Simple file logging {@code Handler}. |
|
* <p> |
|
* The {@code FileHandler} can either write to a specified file, |
|
* or it can write to a rotating set of files. |
|
* <p> |
|
* For a rotating set of files, as each file reaches a given size |
|
* limit, it is closed, rotated out, and a new file opened. |
|
* Successively older files are named by adding "0", "1", "2", |
|
* etc. into the base filename. |
|
* <p> |
|
* By default buffering is enabled in the IO libraries but each log |
|
* record is flushed out when it is complete. |
|
* <p> |
|
* By default the {@code XMLFormatter} class is used for formatting. |
|
* <p> |
|
* <b>Configuration:</b> |
|
* By default each {@code FileHandler} is initialized using the following |
|
* {@code LogManager} configuration properties where {@code <handler-name>} |
|
* refers to the fully-qualified class name of the handler. |
|
* If properties are not defined |
|
* (or have invalid values) then the specified default values are used. |
|
* <ul> |
|
* <li> <handler-name>.level |
|
* specifies the default level for the {@code Handler} |
|
* (defaults to {@code Level.ALL}). </li> |
|
* <li> <handler-name>.filter |
|
* specifies the name of a {@code Filter} class to use |
|
* (defaults to no {@code Filter}). </li> |
|
* <li> <handler-name>.formatter |
|
* specifies the name of a {@code Formatter} class to use |
|
* (defaults to {@code java.util.logging.XMLFormatter}) </li> |
|
* <li> <handler-name>.encoding |
|
* the name of the character set encoding to use (defaults to |
|
* the default platform encoding). </li> |
|
* <li> <handler-name>.limit |
|
* specifies an approximate maximum amount to write (in bytes) |
|
* to any one file. If this is zero, then there is no limit. |
|
* (Defaults to no limit). </li> |
|
* <li> <handler-name>.count |
|
* specifies how many output files to cycle through (defaults to 1). </li> |
|
* <li> <handler-name>.pattern |
|
* specifies a pattern for generating the output file name. See |
|
* below for details. (Defaults to "%h/java%u.log"). </li> |
|
* <li> <handler-name>.append |
|
* specifies whether the FileHandler should append onto |
|
* any existing files (defaults to false). </li> |
|
* <li> <handler-name>.maxLocks |
|
* specifies the maximum number of concurrent locks held by |
|
* FileHandler (defaults to 100). </li> |
|
* </ul> |
|
* <p> |
|
* For example, the properties for {@code FileHandler} would be: |
|
* <ul> |
|
* <li> java.util.logging.FileHandler.level=INFO </li> |
|
* <li> java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter </li> |
|
* </ul> |
|
* <p> |
|
* For a custom handler, e.g. com.foo.MyHandler, the properties would be: |
|
* <ul> |
|
* <li> com.foo.MyHandler.level=INFO </li> |
|
* <li> com.foo.MyHandler.formatter=java.util.logging.SimpleFormatter </li> |
|
* </ul> |
|
* <p> |
|
* A pattern consists of a string that includes the following special |
|
* components that will be replaced at runtime: |
|
* <ul> |
|
* <li> "/" the local pathname separator </li> |
|
* <li> "%t" the system temporary directory </li> |
|
* <li> "%h" the value of the "user.home" system property </li> |
|
* <li> "%g" the generation number to distinguish rotated logs </li> |
|
* <li> "%u" a unique number to resolve conflicts </li> |
|
* <li> "%%" translates to a single percent sign "%" </li> |
|
* </ul> |
|
* If no "%g" field has been specified and the file count is greater |
|
* than one, then the generation number will be added to the end of |
|
* the generated filename, after a dot. |
|
* <p> |
|
* Thus for example a pattern of "%t/java%g.log" with a count of 2 |
|
* would typically cause log files to be written on Solaris to |
|
* /var/tmp/java0.log and /var/tmp/java1.log whereas on Windows 95 they |
|
* would be typically written to C:\TEMP\java0.log and C:\TEMP\java1.log |
|
* <p> |
|
* Generation numbers follow the sequence 0, 1, 2, etc. |
|
* <p> |
|
* Normally the "%u" unique field is set to 0. However, if the {@code FileHandler} |
|
* tries to open the filename and finds the file is currently in use by |
|
* another process it will increment the unique number field and try |
|
* again. This will be repeated until {@code FileHandler} finds a file name that |
|
* is not currently in use. If there is a conflict and no "%u" field has |
|
* been specified, it will be added at the end of the filename after a dot. |
|
* (This will be after any automatically added generation number.) |
|
* <p> |
|
* Thus if three processes were all trying to log to fred%u.%g.txt then |
|
* they might end up using fred0.0.txt, fred1.0.txt, fred2.0.txt as |
|
* the first file in their rotating sequences. |
|
* <p> |
|
* Note that the use of unique ids to avoid conflicts is only guaranteed |
|
* to work reliably when using a local disk file system. |
|
* |
|
* @since 1.4 |
|
*/ |
|
|
|
public class FileHandler extends StreamHandler { |
|
private MeteredStream meter; |
|
private boolean append; |
|
private long limit; |
|
private int count; |
|
private String pattern; |
|
private String lockFileName; |
|
private FileChannel lockFileChannel; |
|
private File files[]; |
|
private static final int MAX_LOCKS = 100; |
|
private int maxLocks = MAX_LOCKS; |
|
private static final Set<String> locks = new HashSet<>(); |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private static final class MeteredStream extends OutputStream { |
|
final OutputStream out; |
|
long written; |
|
|
|
MeteredStream(OutputStream out, long written) { |
|
this.out = out; |
|
this.written = written; |
|
} |
|
|
|
@Override |
|
public void write(int b) throws IOException { |
|
out.write(b); |
|
written++; |
|
} |
|
|
|
@Override |
|
public void write(byte buff[]) throws IOException { |
|
out.write(buff); |
|
written += buff.length; |
|
} |
|
|
|
@Override |
|
public void write(byte buff[], int off, int len) throws IOException { |
|
out.write(buff,off,len); |
|
written += len; |
|
} |
|
|
|
@Override |
|
public void flush() throws IOException { |
|
out.flush(); |
|
} |
|
|
|
@Override |
|
public void close() throws IOException { |
|
out.close(); |
|
} |
|
} |
|
|
|
private void open(File fname, boolean append) throws IOException { |
|
long len = 0; |
|
if (append) { |
|
len = fname.length(); |
|
} |
|
FileOutputStream fout = new FileOutputStream(fname.toString(), append); |
|
BufferedOutputStream bout = new BufferedOutputStream(fout); |
|
meter = new MeteredStream(bout, len); |
|
setOutputStream(meter); |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private void configure() { |
|
LogManager manager = LogManager.getLogManager(); |
|
|
|
String cname = getClass().getName(); |
|
|
|
pattern = manager.getStringProperty(cname + ".pattern", "%h/java%u.log"); |
|
limit = manager.getLongProperty(cname + ".limit", 0); |
|
if (limit < 0) { |
|
limit = 0; |
|
} |
|
count = manager.getIntProperty(cname + ".count", 1); |
|
if (count <= 0) { |
|
count = 1; |
|
} |
|
append = manager.getBooleanProperty(cname + ".append", false); |
|
setLevel(manager.getLevelProperty(cname + ".level", Level.ALL)); |
|
setFilter(manager.getFilterProperty(cname + ".filter", null)); |
|
setFormatter(manager.getFormatterProperty(cname + ".formatter", new XMLFormatter())); |
|
// Initialize maxLocks from the logging.properties file. |
|
|
|
maxLocks = manager.getIntProperty(cname + ".maxLocks", MAX_LOCKS); |
|
if(maxLocks <= 0) { |
|
maxLocks = MAX_LOCKS; |
|
} |
|
try { |
|
setEncoding(manager.getStringProperty(cname +".encoding", null)); |
|
} catch (Exception ex) { |
|
try { |
|
setEncoding(null); |
|
} catch (Exception ex2) { |
|
// doing a setEncoding with null should always work. |
|
// assert false; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public FileHandler() throws IOException, SecurityException { |
|
checkPermission(); |
|
configure(); |
|
// pattern will have been set by configure. check that it's not |
|
|
|
if (pattern.isEmpty()) { |
|
throw new NullPointerException(); |
|
} |
|
openFiles(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public FileHandler(String pattern) throws IOException, SecurityException { |
|
if (pattern.length() < 1 ) { |
|
throw new IllegalArgumentException(); |
|
} |
|
checkPermission(); |
|
configure(); |
|
this.pattern = pattern; |
|
this.limit = 0; |
|
this.count = 1; |
|
openFiles(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public FileHandler(String pattern, boolean append) throws IOException, |
|
SecurityException { |
|
if (pattern.length() < 1 ) { |
|
throw new IllegalArgumentException(); |
|
} |
|
checkPermission(); |
|
configure(); |
|
this.pattern = pattern; |
|
this.limit = 0; |
|
this.count = 1; |
|
this.append = append; |
|
openFiles(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public FileHandler(String pattern, int limit, int count) |
|
throws IOException, SecurityException { |
|
if (limit < 0 || count < 1 || pattern.length() < 1) { |
|
throw new IllegalArgumentException(); |
|
} |
|
checkPermission(); |
|
configure(); |
|
this.pattern = pattern; |
|
this.limit = limit; |
|
this.count = count; |
|
openFiles(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public FileHandler(String pattern, int limit, int count, boolean append) |
|
throws IOException, SecurityException { |
|
this(pattern, (long)limit, count, append); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public FileHandler(String pattern, long limit, int count, boolean append) |
|
throws IOException { |
|
if (limit < 0 || count < 1 || pattern.length() < 1) { |
|
throw new IllegalArgumentException(); |
|
} |
|
checkPermission(); |
|
configure(); |
|
this.pattern = pattern; |
|
this.limit = limit; |
|
this.count = count; |
|
this.append = append; |
|
openFiles(); |
|
} |
|
|
|
private boolean isParentWritable(Path path) { |
|
Path parent = path.getParent(); |
|
if (parent == null) { |
|
parent = path.toAbsolutePath().getParent(); |
|
} |
|
return parent != null && Files.isWritable(parent); |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private void openFiles() throws IOException { |
|
LogManager manager = LogManager.getLogManager(); |
|
manager.checkPermission(); |
|
if (count < 1) { |
|
throw new IllegalArgumentException("file count = " + count); |
|
} |
|
if (limit < 0) { |
|
limit = 0; |
|
} |
|
|
|
|
|
assert pattern != null : "pattern should not be null"; |
|
assert !pattern.isEmpty() : "pattern should not be empty"; |
|
|
|
// We register our own ErrorManager during initialization |
|
|
|
InitializationErrorManager em = new InitializationErrorManager(); |
|
setErrorManager(em); |
|
|
|
// Create a lock file. This grants us exclusive access |
|
|
|
int unique = -1; |
|
for (;;) { |
|
unique++; |
|
if (unique > maxLocks) { |
|
throw new IOException("Couldn't get lock for " + pattern); |
|
} |
|
|
|
lockFileName = generate(pattern, 0, unique).toString() + ".lck"; |
|
// Now try to lock that filename. |
|
// Because some systems (e.g., Solaris) can only do file locks |
|
// between processes (and not within a process), we first check |
|
|
|
synchronized(locks) { |
|
if (locks.contains(lockFileName)) { |
|
// We already own this lock, for a different FileHandler |
|
|
|
continue; |
|
} |
|
|
|
final Path lockFilePath = Paths.get(lockFileName); |
|
FileChannel channel = null; |
|
int retries = -1; |
|
boolean fileCreated = false; |
|
while (channel == null && retries++ < 1) { |
|
try { |
|
channel = FileChannel.open(lockFilePath, |
|
CREATE_NEW, WRITE); |
|
fileCreated = true; |
|
} catch (FileAlreadyExistsException ix) { |
|
// This may be a zombie file left over by a previous |
|
// execution. Reuse it - but only if we can actually |
|
// write to its directory. |
|
// Note that this is a situation that may happen, |
|
|
|
if (Files.isRegularFile(lockFilePath, LinkOption.NOFOLLOW_LINKS) |
|
&& isParentWritable(lockFilePath)) { |
|
try { |
|
channel = FileChannel.open(lockFilePath, |
|
WRITE, APPEND); |
|
} catch (NoSuchFileException x) { |
|
// Race condition - retry once, and if that |
|
// fails again just try the next name in |
|
|
|
continue; |
|
} catch(IOException x) { |
|
// the file may not be writable for us. |
|
|
|
break; |
|
} |
|
} else { |
|
// at this point channel should still be null. |
|
|
|
break; |
|
} |
|
} |
|
} |
|
|
|
if (channel == null) continue; |
|
lockFileChannel = channel; |
|
|
|
boolean available; |
|
try { |
|
available = lockFileChannel.tryLock() != null; |
|
// We got the lock OK. |
|
// At this point we could call File.deleteOnExit(). |
|
// However, this could have undesirable side effects |
|
// as indicated by JDK-4872014. So we will instead |
|
// rely on the fact that close() will remove the lock |
|
// file and that whoever is creating FileHandlers should |
|
// be responsible for closing them. |
|
} catch (IOException ix) { |
|
// We got an IOException while trying to get the lock. |
|
// This normally indicates that locking is not supported |
|
// on the target directory. We have to proceed without |
|
// getting a lock. Drop through, but only if we did |
|
|
|
available = fileCreated; |
|
} catch (OverlappingFileLockException x) { |
|
// someone already locked this file in this VM, through |
|
// some other channel - that is - using something else |
|
// than new FileHandler(...); |
|
|
|
available = false; |
|
} |
|
if (available) { |
|
|
|
locks.add(lockFileName); |
|
break; |
|
} |
|
|
|
|
|
lockFileChannel.close(); |
|
} |
|
} |
|
|
|
files = new File[count]; |
|
for (int i = 0; i < count; i++) { |
|
files[i] = generate(pattern, i, unique); |
|
} |
|
|
|
|
|
if (append) { |
|
open(files[0], true); |
|
} else { |
|
rotate(); |
|
} |
|
|
|
|
|
Exception ex = em.lastException; |
|
if (ex != null) { |
|
if (ex instanceof IOException) { |
|
throw (IOException) ex; |
|
} else if (ex instanceof SecurityException) { |
|
throw (SecurityException) ex; |
|
} else { |
|
throw new IOException("Exception: " + ex); |
|
} |
|
} |
|
|
|
|
|
setErrorManager(new ErrorManager()); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
private File generate(String pattern, int generation, int unique) |
|
throws IOException |
|
{ |
|
return generate(pattern, count, generation, unique); |
|
} |
|
|
|
|
|
static File generate(String pat, int count, int generation, int unique) |
|
throws IOException |
|
{ |
|
Path path = Paths.get(pat); |
|
Path result = null; |
|
boolean sawg = false; |
|
boolean sawu = false; |
|
StringBuilder word = new StringBuilder(); |
|
Path prev = null; |
|
for (Path elem : path) { |
|
if (prev != null) { |
|
prev = prev.resolveSibling(word.toString()); |
|
result = result == null ? prev : result.resolve(prev); |
|
} |
|
String pattern = elem.toString(); |
|
int ix = 0; |
|
word.setLength(0); |
|
while (ix < pattern.length()) { |
|
char ch = pattern.charAt(ix); |
|
ix++; |
|
char ch2 = 0; |
|
if (ix < pattern.length()) { |
|
ch2 = Character.toLowerCase(pattern.charAt(ix)); |
|
} |
|
if (ch == '%') { |
|
if (ch2 == 't') { |
|
String tmpDir = System.getProperty("java.io.tmpdir"); |
|
if (tmpDir == null) { |
|
tmpDir = System.getProperty("user.home"); |
|
} |
|
result = Paths.get(tmpDir); |
|
ix++; |
|
word.setLength(0); |
|
continue; |
|
} else if (ch2 == 'h') { |
|
result = Paths.get(System.getProperty("user.home")); |
|
if (jdk.internal.misc.VM.isSetUID()) { |
|
// Ok, we are in a set UID program. For safety's sake |
|
|
|
throw new IOException("can't use %h in set UID program"); |
|
} |
|
ix++; |
|
word.setLength(0); |
|
continue; |
|
} else if (ch2 == 'g') { |
|
word = word.append(generation); |
|
sawg = true; |
|
ix++; |
|
continue; |
|
} else if (ch2 == 'u') { |
|
word = word.append(unique); |
|
sawu = true; |
|
ix++; |
|
continue; |
|
} else if (ch2 == '%') { |
|
word = word.append('%'); |
|
ix++; |
|
continue; |
|
} |
|
} |
|
word = word.append(ch); |
|
} |
|
prev = elem; |
|
} |
|
|
|
if (count > 1 && !sawg) { |
|
word = word.append('.').append(generation); |
|
} |
|
if (unique > 0 && !sawu) { |
|
word = word.append('.').append(unique); |
|
} |
|
if (word.length() > 0) { |
|
String n = word.toString(); |
|
Path p = prev == null ? Paths.get(n) : prev.resolveSibling(n); |
|
result = result == null ? p : result.resolve(p); |
|
} else if (result == null) { |
|
result = Paths.get(""); |
|
} |
|
|
|
if (path.getRoot() == null) { |
|
return result.toFile(); |
|
} else { |
|
return path.getRoot().resolve(result).toFile(); |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private synchronized void rotate() { |
|
Level oldLevel = getLevel(); |
|
setLevel(Level.OFF); |
|
|
|
super.close(); |
|
for (int i = count-2; i >= 0; i--) { |
|
File f1 = files[i]; |
|
File f2 = files[i+1]; |
|
if (f1.exists()) { |
|
if (f2.exists()) { |
|
f2.delete(); |
|
} |
|
f1.renameTo(f2); |
|
} |
|
} |
|
try { |
|
open(files[0], false); |
|
} catch (IOException ix) { |
|
// We don't want to throw an exception here, but we |
|
|
|
reportError(null, ix, ErrorManager.OPEN_FAILURE); |
|
|
|
} |
|
setLevel(oldLevel); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
@Override |
|
public synchronized void publish(LogRecord record) { |
|
if (!isLoggable(record)) { |
|
return; |
|
} |
|
super.publish(record); |
|
flush(); |
|
if (limit > 0 && (meter.written >= limit || meter.written < 0)) { |
|
// We performed access checks in the "init" method to make sure |
|
// we are only initialized from trusted code. So we assume |
|
// it is OK to write the target files, even if we are |
|
// currently being called from untrusted code. |
|
|
|
AccessController.doPrivileged(new PrivilegedAction<Object>() { |
|
@Override |
|
public Object run() { |
|
rotate(); |
|
return null; |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
@Override |
|
public synchronized void close() throws SecurityException { |
|
super.close(); |
|
|
|
if (lockFileName == null) { |
|
return; |
|
} |
|
try { |
|
|
|
lockFileChannel.close(); |
|
} catch (Exception ex) { |
|
// Problems closing the stream. Punt. |
|
} |
|
synchronized(locks) { |
|
locks.remove(lockFileName); |
|
} |
|
new File(lockFileName).delete(); |
|
lockFileName = null; |
|
lockFileChannel = null; |
|
} |
|
|
|
private static class InitializationErrorManager extends ErrorManager { |
|
Exception lastException; |
|
@Override |
|
public void error(String msg, Exception ex, int code) { |
|
lastException = ex; |
|
} |
|
} |
|
} |