blob: 65e8f761016ab85a7634e9219d055b3599345bc7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2010 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.core.runtime.adaptor;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.security.AccessController;
import java.util.Calendar;
import java.util.Date;
import org.eclipse.core.runtime.internal.adaptor.EclipseEnvironmentInfo;
import org.eclipse.osgi.framework.internal.core.FrameworkProperties;
import org.eclipse.osgi.framework.log.FrameworkLog;
import org.eclipse.osgi.framework.log.FrameworkLogEntry;
import org.eclipse.osgi.framework.util.SecureAction;
import org.osgi.framework.*;
/**
* The FrameworkLog implementation for Eclipse.
* <p>
* Clients may extend this class.
* </p>
* @since 3.1
*/
public class EclipseLog implements FrameworkLog {
private static final String PASSWORD = "-password"; //$NON-NLS-1$
/** The session tag */
protected static final String SESSION = "!SESSION"; //$NON-NLS-1$
/** The entry tag */
protected static final String ENTRY = "!ENTRY"; //$NON-NLS-1$
/** The sub-entry tag */
protected static final String SUBENTRY = "!SUBENTRY"; //$NON-NLS-1$
/** The message tag */
protected static final String MESSAGE = "!MESSAGE"; //$NON-NLS-1$
/** The stacktrace tag */
protected static final String STACK = "!STACK"; //$NON-NLS-1$
/** The line separator used in the log output */
protected static final String LINE_SEPARATOR;
/** The tab character used in the log output */
protected static final String TAB_STRING = "\t"; //$NON-NLS-1$
//Constants for rotating log file
/** The default size a log file can grow before it is rotated */
public static final int DEFAULT_LOG_SIZE = 1000;
/** The default number of backup log files */
public static final int DEFAULT_LOG_FILES = 10;
/** The minimum size limit for log rotation */
public static final int LOG_SIZE_MIN = 10;
/** The system property used to specify the log level */
public static final String PROP_LOG_LEVEL = "eclipse.log.level"; //$NON-NLS-1$
/** The system property used to specify size a log file can grow before it is rotated */
public static final String PROP_LOG_SIZE_MAX = "eclipse.log.size.max"; //$NON-NLS-1$
/** The system property used to specify the maximim number of backup log files to use */
public static final String PROP_LOG_FILE_MAX = "eclipse.log.backup.max"; //$NON-NLS-1$
/** The extension used for log files */
public static final String LOG_EXT = ".log"; //$NON-NLS-1$
/** The extension markup to use for backup log files*/
public static final String BACKUP_MARK = ".bak_"; //$NON-NLS-1$
static {
String s = System.getProperty("line.separator"); //$NON-NLS-1$
LINE_SEPARATOR = s == null ? "\n" : s; //$NON-NLS-1$
}
private static final SecureAction secureAction = (SecureAction) AccessController.doPrivileged(SecureAction.createSecureAction());
/** Indicates if the console messages should be printed to the console (System.out) */
protected boolean consoleLog = false;
/** Indicates if the next log message is part of a new session */
protected boolean newSession = true;
/**
* The File object to store messages. This value may be null.
*/
protected File outFile;
/**
* The Writer to log messages to.
*/
protected Writer writer;
int maxLogSize = DEFAULT_LOG_SIZE; // The value is in KB.
int maxLogFiles = DEFAULT_LOG_FILES;
int backupIdx = 0;
private int logLevel = FrameworkLogEntry.OK;
/**
* Constructs an EclipseLog which uses the specified File to log messages to
* @param outFile a file to log messages to
*/
public EclipseLog(File outFile) {
this.outFile = outFile;
this.writer = null;
readLogProperties();
}
/**
* Constructs an EclipseLog which uses the specified Writer to log messages to
* @param writer a writer to log messages to
*/
public EclipseLog(Writer writer) {
if (writer == null)
// log to System.err by default
this.writer = logForStream(System.err);
else
this.writer = writer;
}
/**
* Constructs an EclipseLog which uses System.err to write log messages to
*
*/
public EclipseLog() {
this((Writer) null);
}
private Throwable getRoot(Throwable t) {
Throwable root = null;
if (t instanceof BundleException)
root = ((BundleException) t).getNestedException();
if (t instanceof InvocationTargetException)
root = ((InvocationTargetException) t).getTargetException();
// skip inner InvocationTargetExceptions and BundleExceptions
if (root instanceof InvocationTargetException || root instanceof BundleException) {
Throwable deeplyNested = getRoot(root);
if (deeplyNested != null)
// if we have something more specific, use it, otherwise keep what we have
root = deeplyNested;
}
return root;
}
/**
* Helper method for writing out argument arrays.
* @param header the header
* @param args the list of arguments
*/
protected void writeArgs(String header, String[] args) throws IOException {
if (args == null || args.length == 0)
return;
write(header);
for (int i = 0; i < args.length; i++) {
//mask out the password argument for security
if (i > 0 && PASSWORD.equals(args[i - 1]))
write(" (omitted)"); //$NON-NLS-1$
else
write(" " + args[i]); //$NON-NLS-1$
}
writeln();
}
/**
* Returns the session timestamp. This is the time the platform was started
* @return the session timestamp
*/
protected String getSessionTimestamp() {
// Main should have set the session start-up timestamp so return that.
// Return the "now" time if not available.
String ts = FrameworkProperties.getProperty("eclipse.startTime"); //$NON-NLS-1$
if (ts != null) {
try {
return getDate(new Date(Long.parseLong(ts)));
} catch (NumberFormatException e) {
// fall through and use the timestamp from right now
}
}
return getDate(new Date());
}
/**
* Writes the session
* @throws IOException if an error occurs writing to the log
*/
protected void writeSession() throws IOException {
write(SESSION);
writeSpace();
String date = getSessionTimestamp();
write(date);
writeSpace();
for (int i = SESSION.length() + date.length(); i < 78; i++) {
write("-"); //$NON-NLS-1$
}
writeln();
// Write out certain values found in System.getProperties()
try {
String key = "eclipse.buildId"; //$NON-NLS-1$
String value = FrameworkProperties.getProperty(key, "unknown"); //$NON-NLS-1$
writeln(key + "=" + value); //$NON-NLS-1$
key = "java.fullversion"; //$NON-NLS-1$
value = System.getProperty(key);
if (value == null) {
key = "java.version"; //$NON-NLS-1$
value = System.getProperty(key);
writeln(key + "=" + value); //$NON-NLS-1$
key = "java.vendor"; //$NON-NLS-1$
value = System.getProperty(key);
writeln(key + "=" + value); //$NON-NLS-1$
} else {
writeln(key + "=" + value); //$NON-NLS-1$
}
} catch (Exception e) {
// If we're not allowed to get the values of these properties
// then just skip over them.
}
// The Bootloader has some information that we might be interested in.
write("BootLoader constants: OS=" + EclipseEnvironmentInfo.getDefault().getOS()); //$NON-NLS-1$
write(", ARCH=" + EclipseEnvironmentInfo.getDefault().getOSArch()); //$NON-NLS-1$
write(", WS=" + EclipseEnvironmentInfo.getDefault().getWS()); //$NON-NLS-1$
writeln(", NL=" + EclipseEnvironmentInfo.getDefault().getNL()); //$NON-NLS-1$
// Add the command-line arguments used to invoke the platform
// XXX: this includes runtime-private arguments - should we do that?
writeArgs("Framework arguments: ", EclipseEnvironmentInfo.getDefault().getNonFrameworkArgs()); //$NON-NLS-1$
writeArgs("Command-line arguments: ", EclipseEnvironmentInfo.getDefault().getCommandLineArgs()); //$NON-NLS-1$
}
public void close() {
try {
if (writer != null) {
Writer tmpWriter = writer;
writer = null;
tmpWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* If a File is used to log messages to then the File opened and a Writer is created
* to log messages to.
*/
protected void openFile() {
if (writer == null) {
if (outFile != null) {
try {
writer = logForStream(secureAction.getFileOutputStream(outFile, true));
} catch (IOException e) {
writer = logForStream(System.err);
}
} else {
writer = logForStream(System.err);
}
}
}
/**
* If a File is used to log messages to then the writer is closed.
*/
protected void closeFile() {
if (outFile != null) {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
// we cannot log here; just print the stacktrace.
e.printStackTrace();
}
writer = null;
}
}
}
public void log(FrameworkEvent frameworkEvent) {
Bundle b = frameworkEvent.getBundle();
Throwable t = frameworkEvent.getThrowable();
String entry = b.getSymbolicName() == null ? b.getLocation() : b.getSymbolicName();
int severity;
switch (frameworkEvent.getType()) {
case FrameworkEvent.INFO :
severity = FrameworkLogEntry.INFO;
break;
case FrameworkEvent.ERROR :
severity = FrameworkLogEntry.ERROR;
break;
case FrameworkEvent.WARNING :
severity = FrameworkLogEntry.WARNING;
break;
default :
severity = FrameworkLogEntry.OK;
}
FrameworkLogEntry logEntry = new FrameworkLogEntry(entry, severity, 0, "", 0, t, null); //$NON-NLS-1$
log(logEntry);
}
public synchronized void log(FrameworkLogEntry logEntry) {
if (logEntry == null)
return;
if (!isLoggable(logEntry))
return;
try {
checkLogFileSize();
openFile();
if (newSession) {
writeSession();
newSession = false;
}
writeLog(0, logEntry);
writer.flush();
} catch (Exception e) {
// any exceptions during logging should be caught
System.err.println("An exception occurred while writing to the platform log:");//$NON-NLS-1$
e.printStackTrace(System.err);
System.err.println("Logging to the console instead.");//$NON-NLS-1$
//we failed to write, so dump log entry to console instead
try {
writer = logForStream(System.err);
writeLog(0, logEntry);
writer.flush();
} catch (Exception e2) {
System.err.println("An exception occurred while logging to the console:");//$NON-NLS-1$
e2.printStackTrace(System.err);
}
} finally {
closeFile();
}
}
public synchronized void setWriter(Writer newWriter, boolean append) {
setOutput(null, newWriter, append);
}
public synchronized void setFile(File newFile, boolean append) throws IOException {
if (newFile != null && !newFile.equals(this.outFile)) {
// If it's a new file, then reset.
readLogProperties();
backupIdx = 0;
}
setOutput(newFile, null, append);
FrameworkProperties.setProperty(EclipseStarter.PROP_LOGFILE, newFile == null ? "" : newFile.getAbsolutePath()); //$NON-NLS-1$
}
public synchronized File getFile() {
return outFile;
}
public void setConsoleLog(boolean consoleLog) {
this.consoleLog = consoleLog;
}
private void setOutput(File newOutFile, Writer newWriter, boolean append) {
if (newOutFile == null || !newOutFile.equals(this.outFile)) {
if (this.writer != null) {
try {
this.writer.close();
} catch (IOException e) {
e.printStackTrace();
}
this.writer = null;
}
// Append old outFile to newWriter. We only attempt to do this
// if the current Writer is backed by a File and this is not
// a new session.
File oldOutFile = this.outFile;
this.outFile = newOutFile;
this.writer = newWriter;
boolean copyFailed = false;
if (append && oldOutFile != null && oldOutFile.isFile()) {
Reader fileIn = null;
try {
openFile();
fileIn = new InputStreamReader(secureAction.getFileInputStream(oldOutFile), "UTF-8"); //$NON-NLS-1$
copyReader(fileIn, this.writer);
} catch (IOException e) {
copyFailed = true;
e.printStackTrace();
} finally {
if (fileIn != null) {
try {
fileIn.close();
} catch (IOException e) {
e.printStackTrace();
}
// delete the old file if copying didn't fail
if (!copyFailed)
oldOutFile.delete();
}
closeFile();
}
}
}
}
private void copyReader(Reader reader, Writer aWriter) throws IOException {
char buffer[] = new char[1024];
int count;
while ((count = reader.read(buffer, 0, buffer.length)) > 0) {
aWriter.write(buffer, 0, count);
}
}
/**
* Returns a date string using the correct format for the log.
* @param date the Date to format
* @return a date string.
*/
protected String getDate(Date date) {
Calendar c = Calendar.getInstance();
c.setTime(date);
StringBuffer sb = new StringBuffer();
appendPaddedInt(c.get(Calendar.YEAR), 4, sb).append('-');
appendPaddedInt(c.get(Calendar.MONTH) + 1, 2, sb).append('-');
appendPaddedInt(c.get(Calendar.DAY_OF_MONTH), 2, sb).append(' ');
appendPaddedInt(c.get(Calendar.HOUR_OF_DAY), 2, sb).append(':');
appendPaddedInt(c.get(Calendar.MINUTE), 2, sb).append(':');
appendPaddedInt(c.get(Calendar.SECOND), 2, sb).append('.');
appendPaddedInt(c.get(Calendar.MILLISECOND), 3, sb);
return sb.toString();
}
private StringBuffer appendPaddedInt(int value, int pad, StringBuffer buffer) {
pad = pad - 1;
if (pad == 0)
return buffer.append(Integer.toString(value));
int padding = (int) Math.pow(10, pad);
if (value >= padding)
return buffer.append(Integer.toString(value));
while (padding > value && padding > 1) {
buffer.append('0');
padding = padding / 10;
}
buffer.append(value);
return buffer;
}
/**
* Returns a stacktrace string using the correct format for the log
* @param t the Throwable to get the stacktrace for
* @return a stacktrace string
*/
protected String getStackTrace(Throwable t) {
if (t == null)
return null;
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
t.printStackTrace(pw);
// ensure the root exception is fully logged
Throwable root = getRoot(t);
if (root != null) {
pw.println("Root exception:"); //$NON-NLS-1$
root.printStackTrace(pw);
}
return sw.toString();
}
/**
* Returns a Writer for the given OutputStream
* @param output an OutputStream to use for the Writer
* @return a Writer for the given OutputStream
*/
protected Writer logForStream(OutputStream output) {
try {
return new BufferedWriter(new OutputStreamWriter(output, "UTF-8")); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
return new BufferedWriter(new OutputStreamWriter(output));
}
}
/**
* Writes the log entry to the log using the specified depth. A depth value of 0
* idicates that the log entry is the root entry. Any value greater than 0 indicates
* a sub-entry.
* @param depth the depth of th entry
* @param entry the entry to log
* @throws IOException if any error occurs writing to the log
*/
protected void writeLog(int depth, FrameworkLogEntry entry) throws IOException {
writeEntry(depth, entry);
writeMessage(entry);
writeStack(entry);
FrameworkLogEntry[] children = entry.getChildren();
if (children != null) {
for (int i = 0; i < children.length; i++) {
writeLog(depth + 1, children[i]);
}
}
}
/**
* Writes the ENTRY or SUBENTRY header for an entry. A depth value of 0
* indicates that the log entry is the root entry. Any value greater than 0 indicates
* a sub-entry.
* @param depth the depth of th entry
* @param entry the entry to write the header for
* @throws IOException if any error occurs writing to the log
*/
protected void writeEntry(int depth, FrameworkLogEntry entry) throws IOException {
if (depth == 0) {
writeln(); // write a blank line before all !ENTRY tags bug #64406
write(ENTRY);
} else {
write(SUBENTRY);
writeSpace();
write(Integer.toString(depth));
}
writeSpace();
write(entry.getEntry());
writeSpace();
write(Integer.toString(entry.getSeverity()));
writeSpace();
write(Integer.toString(entry.getBundleCode()));
writeSpace();
write(getDate(new Date()));
writeln();
}
/**
* Writes the MESSAGE header to the log for the given entry.
* @param entry the entry to write the message for
* @throws IOException if any error occurs writing to the log
*/
protected void writeMessage(FrameworkLogEntry entry) throws IOException {
write(MESSAGE);
writeSpace();
writeln(entry.getMessage());
}
/**
* Writes the STACK header to the log for the given entry.
* @param entry the entry to write the stacktrace for
* @throws IOException if any error occurs writing to the log
*/
protected void writeStack(FrameworkLogEntry entry) throws IOException {
Throwable t = entry.getThrowable();
if (t != null) {
String stack = getStackTrace(t);
write(STACK);
writeSpace();
write(Integer.toString(entry.getStackCode()));
writeln();
write(stack);
}
}
/**
* Writes the given message to the log.
* @param message the message
* @throws IOException if any error occurs writing to the log
*/
protected void write(String message) throws IOException {
if (message != null) {
writer.write(message);
if (consoleLog)
System.out.print(message);
}
}
/**
* Writes the given message to the log and a newline.
* @param s the message
* @throws IOException if any error occurs writing to the log
*/
protected void writeln(String s) throws IOException {
write(s);
writeln();
}
/**
* Writes a newline log.
* @throws IOException if any error occurs writing to the log
*/
protected void writeln() throws IOException {
write(LINE_SEPARATOR);
}
/**
* Writes a space to the log.
* @throws IOException if any error occurs writing to the log
*/
protected void writeSpace() throws IOException {
write(" "); //$NON-NLS-1$
}
/**
* Checks the log file size. If the log file size reaches the limit then the log
* is rotated
* @return false if an error occured trying to rotate the log
*/
protected boolean checkLogFileSize() {
if (maxLogSize == 0)
return true; // no size limitation.
boolean isBackupOK = true;
if (outFile != null) {
if ((secureAction.length(outFile) >> 10) > maxLogSize) { // Use KB as file size unit.
String logFilename = outFile.getAbsolutePath();
// Delete old backup file that will be replaced.
String backupFilename = ""; //$NON-NLS-1$
if (logFilename.toLowerCase().endsWith(LOG_EXT)) {
backupFilename = logFilename.substring(0, logFilename.length() - LOG_EXT.length()) + BACKUP_MARK + backupIdx + LOG_EXT;
} else {
backupFilename = logFilename + BACKUP_MARK + backupIdx;
}
File backupFile = new File(backupFilename);
if (backupFile.exists()) {
if (!backupFile.delete()) {
System.err.println("Error when trying to delete old log file: " + backupFile.getName());//$NON-NLS-1$
if (backupFile.renameTo(new File(backupFile.getAbsolutePath() + System.currentTimeMillis()))) {
System.err.println("So we rename it to filename: " + backupFile.getName()); //$NON-NLS-1$
} else {
System.err.println("And we also cannot rename it!"); //$NON-NLS-1$
isBackupOK = false;
}
}
}
// Rename current log file to backup one.
boolean isRenameOK = outFile.renameTo(backupFile);
if (!isRenameOK) {
System.err.println("Error when trying to rename log file to backup one."); //$NON-NLS-1$
isBackupOK = false;
}
File newFile = new File(logFilename);
setOutput(newFile, null, false);
// Write a new SESSION header to new log file.
openFile();
try {
writeSession();
writeln();
writeln("This is a continuation of log file " + backupFile.getAbsolutePath());//$NON-NLS-1$
writeln("Created Time: " + getDate(new Date(System.currentTimeMillis()))); //$NON-NLS-1$
writer.flush();
} catch (IOException ioe) {
ioe.printStackTrace(System.err);
}
closeFile();
backupIdx = (++backupIdx) % maxLogFiles;
}
}
return isBackupOK;
}
/**
* Reads the PROP_LOG_SIZE_MAX and PROP_LOG_FILE_MAX properties.
*/
protected void readLogProperties() {
String newMaxLogSize = secureAction.getProperty(PROP_LOG_SIZE_MAX);
if (newMaxLogSize != null) {
maxLogSize = Integer.parseInt(newMaxLogSize);
if (maxLogSize != 0 && maxLogSize < LOG_SIZE_MIN) {
// If the value is '0', then it means no size limitation.
// Also, make sure no inappropriate(too small) assigned value.
maxLogSize = LOG_SIZE_MIN;
}
}
String newMaxLogFiles = secureAction.getProperty(PROP_LOG_FILE_MAX);
if (newMaxLogFiles != null) {
maxLogFiles = Integer.parseInt(newMaxLogFiles);
if (maxLogFiles < 1) {
// Make sure no invalid assigned value. (at least >= 1)
maxLogFiles = DEFAULT_LOG_FILES;
}
}
String newLogLevel = secureAction.getProperty(PROP_LOG_LEVEL);
if (newLogLevel != null) {
if (newLogLevel.equals("ERROR")) //$NON-NLS-1$
logLevel = FrameworkLogEntry.ERROR;
else if (newLogLevel.equals("WARNING")) //$NON-NLS-1$
logLevel = FrameworkLogEntry.ERROR | FrameworkLogEntry.WARNING;
else if (newLogLevel.equals("INFO")) //$NON-NLS-1$
logLevel = FrameworkLogEntry.INFO | FrameworkLogEntry.ERROR | FrameworkLogEntry.WARNING | FrameworkLogEntry.CANCEL;
else
logLevel = FrameworkLogEntry.OK; // OK (0) means log everything
}
}
/**
* Determines if the log entry should be logged based on log level.
*/
private boolean isLoggable(FrameworkLogEntry entry) {
if (logLevel == 0)
return true;
return (entry.getSeverity() & logLevel) != 0;
}
}