blob: f4c9b1897786d936d7559e7d462ce5e3d7ec0256 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2015 Wind River Systems, Inc. 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:
* Wind River Systems - initial API and implementation
*******************************************************************************/
package org.eclipse.tcf.te.tcf.log.core.manager;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.osgi.util.NLS;
import org.eclipse.tcf.protocol.IPeer;
import org.eclipse.tcf.te.runtime.concurrent.util.ExecutorsUtil;
import org.eclipse.tcf.te.runtime.events.EventManager;
import org.eclipse.tcf.te.tcf.log.core.activator.CoreBundleActivator;
import org.eclipse.tcf.te.tcf.log.core.events.MonitorEvent;
import org.eclipse.tcf.te.tcf.log.core.interfaces.IPreferenceKeys;
import org.eclipse.tcf.te.tcf.log.core.internal.nls.Messages;
/**
* TCF logging log manager implementation.
*/
public final class LogManager {
/**
* Time format representing date and time with milliseconds.
*/
public final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); //$NON-NLS-1$
// Maps file writer per log file base name
private final Map<String, FileWriter> fileWriterMap = new HashMap<String, FileWriter>();
// Maximum log file size in bytes
private long maxFileSize;
// Maximum number of files in cycle
private int maxInCycle;
/*
* Thread save singleton instance creation.
*/
private static class LazyInstance {
public static LogManager instance = new LogManager();
}
/**
* Constructor.
*/
/* default */ LogManager() {
super();
// initialize from preferences
initializeFromPreferences();
}
/**
* Returns the singleton instance.
*/
public static LogManager getInstance() {
return LazyInstance.instance;
}
/**
* Dispose the log manager instance.
* <p>
* Note: This method is callable from every thread.
*/
public void dispose() {
// Close all still open file writer instances
for (FileWriter writer : fileWriterMap.values()) {
try {
writer.flush();
writer.close();
} catch (IOException e) {
/* ignored on purpose */
}
}
fileWriterMap.clear();
}
/**
* Initialize the log manager based on the current
* preference settings
*/
private void initializeFromPreferences() {
String fileSize = CoreBundleActivator.getScopedPreferences().getString(IPreferenceKeys.PREF_MAX_FILE_SIZE);
if (fileSize == null) fileSize = "5M"; //$NON-NLS-1$
try {
// If the last character is either K, M or G -> convert to bytes
char lastChar = fileSize.toUpperCase().charAt(fileSize.length() - 1);
if ('K' == lastChar || 'M' == lastChar || 'G' == lastChar) {
maxFileSize = Long.parseLong(fileSize.substring(0, fileSize.length() - 1));
switch (lastChar) {
case 'K':
maxFileSize = maxFileSize * 1024;
break;
case 'M':
maxFileSize = maxFileSize * 1024 * 1024;
break;
case 'G':
maxFileSize = maxFileSize * 1024 * 1024 * 1024;
break;
}
} else {
maxFileSize = Long.parseLong(fileSize);
}
} catch (NumberFormatException e) {
maxFileSize = 5242880L;
}
maxInCycle = CoreBundleActivator.getScopedPreferences().getInt(IPreferenceKeys.PREF_MAX_FILES_IN_CYCLE);
if (maxInCycle <= 0) maxInCycle = 5;
}
/**
* Returns the file writer instance to use for the given channel.
* <p>
* Note: This method is callable from the executor thread only.
*
* @param logname The log name or <code>null</code>.
* @param peer The peer. Must not be <code>null</code>.
* @return The file writer instance or <code>null</code>.
*/
public FileWriter getWriter(String logname, IPeer peer) {
Assert.isNotNull(peer);
Assert.isTrue(ExecutorsUtil.isExecutorThread(), "Illegal Thread Access"); //$NON-NLS-1$
// Before looking up the writer, check the file limits
checkLimits(logname, peer);
if (logname == null) logname = getLogName(peer);
FileWriter writer = logname != null ? fileWriterMap.get(logname) : null;
if (writer == null && logname != null) {
// Create the writer
IPath path = getLogDir();
if (path != null) {
path = path.append(logname + ".log"); //$NON-NLS-1$
try {
writer = new FileWriter(path.toFile(), true);
fileWriterMap.put(logname, writer);
} catch (IOException e) {
/* ignored on purpose */
}
}
}
return writer;
}
/**
* Close the writer instance used for the given channel.
* <p>
* Note: This method is callable from the executor thread only.
*
* @param logname The log name or <code>null</code>.
* @param peer The peer. Must not be <code>null</code>.
* @param message The last message to write or <code>null</code>.
*/
public void closeWriter(String logname, IPeer peer, String message) {
Assert.isNotNull(peer);
Assert.isTrue(ExecutorsUtil.isExecutorThread(), "Illegal Thread Access"); //$NON-NLS-1$
// Remove the writer from the map
if (logname == null) logname = getLogName(peer);
FileWriter writer = logname != null ? fileWriterMap.remove(logname) : null;
if (writer != null) {
try {
// If specified, write the last message.
if (message != null) {
writer.write(message);
writer.write("\n"); //$NON-NLS-1$
}
} catch (IOException e) {
/* ignored on purpose */
} finally {
try {
writer.flush();
writer.close();
} catch (IOException e) {
/* ignored on purpose */
}
}
}
}
/**
* Returns the log file base name for the given peer.
* <p>
* Note: This method is callable from every thread.
*
* @param peer The peer. Must not be <code>null</code>.
* @return The log file base name.
*/
public String getLogName(IPeer peer) {
Assert.isNotNull(peer);
String logName = null;
// Get the peer name
logName = peer.getName();
if (logName != null) {
// Get the peer host IP address
String ip = peer.getAttributes().get(IPeer.ATTR_IP_HOST);
// Fallback: The peer id
if (ip == null || "".equals(ip.trim())) { //$NON-NLS-1$
ip = peer.getID();
}
// Append the peer host IP address
if (ip != null && !"".equals(ip.trim())) { //$NON-NLS-1$
logName += " " + ip.trim(); //$NON-NLS-1$
}
// Unify name and replace all undesired characters with '_'
logName = makeValid(logName);
}
return logName;
}
/**
* Replaces a set of predefined patterns with underscore to
* make a valid name.
* <p>
* Note: This method is callable from every thread.
*
* @param name The name. Must not be <code>null</code>.
* @return The modified name.
*/
public String makeValid(String name) {
Assert.isNotNull(name);
String result = name.replaceAll("\\s", "_"); //$NON-NLS-1$ //$NON-NLS-2$
result = result.replaceAll("[:/\\;,\\[\\]\\(\\)]", "_"); //$NON-NLS-1$ //$NON-NLS-2$
return result;
}
/**
* Returns the log directory.
* <p>
* Note: This method is callable from every thread.
*
* @return The log directory.
*/
public IPath getLogDir() {
IPath logDir = null;
// In some rare cases, we end up here with an NPE on shutdown.
// So it does not hurt to check it.
if (CoreBundleActivator.getDefault() == null) return logDir;
try {
File file = CoreBundleActivator.getDefault().getStateLocation().append(".logs").toFile(); //$NON-NLS-1$
boolean exists = file.exists();
if (!exists) exists = file.mkdirs();
if (exists && file.canRead() && file.isDirectory()) {
logDir = new Path(file.toString());
}
} catch (IllegalStateException e) {
// Ignored: Workspace less environment (-data @none)
}
if (logDir == null) {
// First fallback: ${HOME}/.tcf/.logs
File file = new Path(System.getProperty("user.home")).append(".tcf/.logs").toFile(); //$NON-NLS-1$ //$NON-NLS-2$
boolean exists = file.exists();
if (!exists) exists = file.mkdirs();
if (exists && file.canRead() && file.isDirectory()) {
logDir = new Path(file.toString());
}
}
if (logDir == null) {
// Second fallback: ${TEMP}/.tcf/.logs
File file = new Path(System.getProperty("java.io.tmpdir")).append(".tcf/.logs").toFile(); //$NON-NLS-1$ //$NON-NLS-2$
boolean exists = file.exists();
if (!exists) exists = file.mkdirs();
if (exists && file.canRead() && file.isDirectory()) {
logDir = new Path(file.toString());
}
}
return logDir;
}
/**
* Checks the limits set by the preferences.
*
* @param logname The log name or <code>null</code>.
* @param peer The peer. Must not be <code>null</code>.
* @return The checked file writer instance.
*/
private void checkLimits(String logname, IPeer peer) {
Assert.isNotNull(peer);
String logName = getLogName(peer);
if (logName != null && !"".equals(logName.trim())) { //$NON-NLS-1$
IPath path = getLogDir();
if (path != null) {
IPath fullPath = path.append(logName + ".log"); //$NON-NLS-1$
File file = fullPath.toFile();
if (file.exists()) {
long size = file.length();
if (size >= maxFileSize) {
// Max log file size reached -> cycle files
// If there is an active writer, flush and close the writer
closeWriter(logname, peer, null);
// Determine if the maximum number of files in the cycle has been reached
File maxFileInCycle = path.append(logName + "_" + maxInCycle + ".log").toFile(); //$NON-NLS-1$ //$NON-NLS-2$
if (maxFileInCycle.exists()) {
// We have to rotate the full cycle, first in cycle to be removed.
int no = 1;
File fileInCycle = path.append(logName + "_" + no + ".log").toFile(); //$NON-NLS-1$ //$NON-NLS-2$
boolean rc = fileInCycle.delete();
if (rc) {
while (no <= maxInCycle) {
no++;
fileInCycle = path.append(logName + "_" + no + ".log").toFile(); //$NON-NLS-1$ //$NON-NLS-2$
File renameTo = path.append(logName + "_" + (no - 1) + ".log").toFile(); //$NON-NLS-1$ //$NON-NLS-2$
rc = fileInCycle.renameTo(renameTo);
if (!rc) break;
}
// Rename the log file if the rotate succeeded,
// Delete the log file if not.
rc = rc ? file.renameTo(maxFileInCycle) : file.delete();
if (!rc && Platform.inDebugMode()) {
System.err.println(NLS.bind(Messages.LogManager_error_renameFailed, fullPath.toOSString(), maxFileInCycle.getAbsolutePath()));
}
}
} else {
// Not at the limit, find the next file name in the cycle
int no = 1;
File fileInCycle = path.append(logName + "_" + no + ".log").toFile(); //$NON-NLS-1$ //$NON-NLS-2$
while (fileInCycle.exists()) {
no++;
fileInCycle = path.append(logName + "_" + no + ".log").toFile(); //$NON-NLS-1$ //$NON-NLS-2$
}
Assert.isTrue(no <= maxInCycle);
// Rename the log file
boolean rc = file.renameTo(fileInCycle);
if (!rc && Platform.inDebugMode()) {
System.err.println(NLS.bind(Messages.LogManager_error_renameFailed, fullPath.toOSString(), fileInCycle.getAbsolutePath()));
}
}
}
}
}
}
}
/**
* Sends an event to the monitor signaling the given message and type.
*
* @param peer The peer. Must not be <code>null</code>.
* @param type The message type. Must not be <code>null</code>.
* @param message The message. Must not be <code>null</code>.
*/
public void monitor(IPeer peer, MonitorEvent.Type type, MonitorEvent.Message message) {
Assert.isNotNull(peer);
Assert.isNotNull(type);
Assert.isNotNull(message);
Assert.isTrue(ExecutorsUtil.isExecutorThread(), "Illegal Thread Access"); //$NON-NLS-1$
// If monitoring is not enabled, return immediately
if (!CoreBundleActivator.getScopedPreferences().getBoolean(IPreferenceKeys.PREF_MONITOR_ENABLED)) {
return;
}
// The source of a monitor event is the peer.
MonitorEvent event = new MonitorEvent(peer, type, message);
EventManager.getInstance().fireEvent(event);
}
}