/*******************************************************************************
* Copyright (c) 2011 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.tm.te.tcf.log.core.internal;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Date;
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.tm.tcf.protocol.IChannel;
import org.eclipse.tm.tcf.protocol.IPeer;
import org.eclipse.tm.tcf.protocol.Protocol;
import org.eclipse.tm.te.tcf.core.Tcf;
import org.eclipse.tm.te.tcf.core.interfaces.listeners.IChannelStateChangeListener;
import org.eclipse.tm.te.tcf.core.interfaces.listeners.IProtocolStateChangeListener;
import org.eclipse.tm.te.tcf.log.core.activator.CoreBundleActivator;
import org.eclipse.tm.te.tcf.log.core.interfaces.IPreferenceKeys;
import org.eclipse.tm.te.tcf.log.core.internal.listener.ChannelStateChangeListener;
import org.eclipse.tm.te.tcf.log.core.internal.listener.ChannelTraceListener;
import org.eclipse.tm.te.tcf.log.core.internal.nls.Messages;
/**
* TCF logging log manager implementation.
*/
public final class LogManager implements IProtocolStateChangeListener {
// Reference to the channel state change listener
private IChannelStateChangeListener channelStateChangeListener;
// 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.
*/
public void dispose() {
String message = NLS.bind(Messages.ChannelTraceListener_logManagerDispose_message,
ChannelTraceListener.DATE_FORMAT.format(new Date(System.currentTimeMillis())));
for (FileWriter writer : fileWriterMap.values()) {
try {
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 */
}
}
}
fileWriterMap.clear();
}
/**
* Initialize the log manager based on the current
* preference settings
*/
private void initializeFromPreferences() {
String fileSize = Platform.getPreferencesService().getString(CoreBundleActivator.getUniqueIdentifier(),
IPreferenceKeys.PREF_MAX_FILE_SIZE, "5M", null); //$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 = Platform.getPreferencesService().getInt(CoreBundleActivator.getUniqueIdentifier(),
IPreferenceKeys.PREF_MAX_FILES_IN_CYCLE, 5, null);
}
/**
* Create, register and initialize the listeners.
* <p>
* <b>Note:</b> This method is supposed to be called from {@link Startup} only!
*/
/* default */ final void initListeners() {
Assert.isTrue(Protocol.isDispatchThread());
// If the channel state change listener instance has been created
// already, there is nothing left to do here
if (channelStateChangeListener != null) return;
// Register ourself as protocol change listener
Tcf.addProtocolStateChangeListener(this);
// Create and register the channel state change listener
channelStateChangeListener = new ChannelStateChangeListener();
Tcf.addChannelStateChangeListener(channelStateChangeListener);
}
/* (non-Javadoc)
* @see org.eclipse.tm.te.tcf.core.interfaces.listeners.IProtocolStateChangeListener#stateChanged(boolean)
*/
@Override
public void stateChanged(boolean state) {
Assert.isTrue(Protocol.isDispatchThread());
// On shutdown, get the listener removed and disposed
if (!state) {
Tcf.removeChannelStateChangeListener(channelStateChangeListener);
channelStateChangeListener = null;
Tcf.removeProtocolStateChangeListener(this);
}
}
/**
* Returns the file writer instance to use for the given channel.
*
* @param channel The channel. Must not be <code>null</code>.
* @return The file writer instance or <code>null</code>.
*/
public FileWriter getWriter(IChannel channel) {
Assert.isNotNull(channel);
Assert.isTrue(Protocol.isDispatchThread());
// Before looking up the writer, check the file limits
checkLimits(channel);
String logName = getLogName(channel);
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.
*
* @param channel The channel. Must not be <code>null</code>.
* @param message The last message to write or <code>null</code>.
*/
public void closeWriter(IChannel channel, String message) {
Assert.isNotNull(channel);
Assert.isTrue(Protocol.isDispatchThread());
// Remove the writer from the map
String logName = getLogName(channel);
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 id.
*
* @param channel The channel. Must not be <code>null</code>.
* @return The log file base name.
*/
public String getLogName(IChannel channel) {
Assert.isNotNull(channel);
Assert.isTrue(Protocol.isDispatchThread());
String logName = null;
IPeer peer = channel.getRemotePeer();
if (peer != null) {
// Get the peer name
logName = peer.getName();
// 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.
*
* @param name The name. Must not be <code>null</code>.
* @return The modified name.
*/
private 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.
*
* @return The log directory.
*/
public IPath getLogDir() {
IPath logDir = null;
try {
File file = CoreBundleActivator.getDefault().getStateLocation().append(".logs").toFile(); //$NON-NLS-1$
if (!file.exists()) file.mkdirs();
if (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$
if (!file.exists()) file.mkdirs();
if (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$
if (!file.exists()) file.mkdirs();
if (file.canRead() && file.isDirectory()) {
logDir = new Path(file.toString());
}
}
return logDir;
}
/**
* Checks the limits set by the preferences.
*
* @param channel The channel. Must not be <code>null</code>.
* @return The checked file writer instance.
*/
private void checkLimits(IChannel channel) {
Assert.isNotNull(channel);
String logName = getLogName(channel);
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(channel, 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$
fileInCycle.delete();
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$
fileInCycle.renameTo(renameTo);
}
// Rename the log file
file.renameTo(maxFileInCycle);
} 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
file.renameTo(fileInCycle);
}
}
}
}
}
}
}