/*
* Copyright 2016-present Facebook, Inc.
*
* Licensed 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 com.facebook.buck.log;
import com.facebook.buck.model.BuildId;
import com.facebook.buck.util.DirectoryCleaner;
import com.facebook.buck.util.Verbosity;
import com.google.common.collect.Lists;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import javax.annotation.Nullable;
public class GlobalStateManager {
private static final Logger LOG = Logger.get(GlobalStateManager.class);
private static final GlobalStateManager SINGLETON = new GlobalStateManager();
private static final String DEFAULT_LOG_FILE_WRITER_KEY = "DEFAULT";
private static final DirectoryCleaner LOG_FILE_DIR_CLEANER = LogFileHandler.newCleaner();
// Shared global state.
private final ConcurrentMap<Long, String> threadIdToCommandId;
// Global state required by the ConsoleHandler.
private final ConcurrentMap<String, ConsoleHandlerState.Writer> commandIdToConsoleHandlerWriter;
private final ConcurrentMap<String, Level> commandIdToConsoleHandlerLevel;
// Global state required by the LogFileHandler.
private final ConcurrentMap<String, java.io.Writer> commandIdToLogFileHandlerWriter;
private final ConcurrentMap<String, Boolean> commandIdToIsSuperconsoleEnabled;
private final ConcurrentMap<String, Boolean> commandIdToIsDaemon;
public static GlobalStateManager singleton() {
return SINGLETON;
}
public GlobalStateManager() {
this.threadIdToCommandId = new ConcurrentHashMap<>();
this.commandIdToConsoleHandlerWriter = new ConcurrentHashMap<>();
this.commandIdToConsoleHandlerLevel = new ConcurrentHashMap<>();
this.commandIdToLogFileHandlerWriter = new ConcurrentHashMap<>();
this.commandIdToIsSuperconsoleEnabled = new ConcurrentHashMap<>();
this.commandIdToIsDaemon = new ConcurrentHashMap<>();
ReferenceCountedWriter defaultWriter =
rotateDefaultLogFileWriter(
InvocationInfo.of(
new BuildId(),
false,
false,
"launch",
new String[0],
new String[0],
LogConfigSetup.DEFAULT_SETUP.getLogDir())
.getLogFilePath());
putReferenceCountedWriter(DEFAULT_LOG_FILE_WRITER_KEY, defaultWriter);
}
public LoggerIsMappedToThreadScope setupLoggers(
final InvocationInfo info,
OutputStream consoleHandlerStream,
final OutputStream consoleHandlerOriginalStream,
final Verbosity consoleHandlerVerbosity) {
final long threadId = Thread.currentThread().getId();
final String commandId = info.getCommandId();
ReferenceCountedWriter defaultWriter = rotateDefaultLogFileWriter(info.getLogFilePath());
ReferenceCountedWriter newWriter = defaultWriter.newReference();
// Put defaultWriter to map only after newWriter has been created. Otherwise defaultWriter may
// get closed before newWriter was created due to concurrency.
putReferenceCountedWriter(DEFAULT_LOG_FILE_WRITER_KEY, defaultWriter);
putReferenceCountedWriter(commandId, newWriter);
// Setup the shared state.
threadIdToCommandId.putIfAbsent(threadId, commandId);
// Setup the ConsoleHandler state.
commandIdToConsoleHandlerWriter.put(
commandId, ConsoleHandler.utf8OutputStreamWriter(consoleHandlerStream));
if (Verbosity.ALL.equals(consoleHandlerVerbosity)) {
commandIdToConsoleHandlerLevel.put(commandId, Level.ALL);
}
commandIdToIsSuperconsoleEnabled.put(commandId, info.getSuperConsoleEnabled());
commandIdToIsDaemon.put(commandId, info.getIsDaemon());
// Setup the LogFileHandler state.
Path logDirectory = info.getLogDirectoryPath();
try {
Files.createDirectories(logDirectory);
} catch (IOException e) {
LOG.error(
e, "Failed to created 'per command log directory': [%s]", logDirectory.toAbsolutePath());
}
return new LoggerIsMappedToThreadScope() {
@Override
public Closeable setWriter(ConsoleHandlerState.Writer writer) {
final ConsoleHandlerState.Writer previousWriter =
commandIdToConsoleHandlerWriter.get(commandId);
commandIdToConsoleHandlerWriter.put(commandId, writer);
return () -> commandIdToConsoleHandlerWriter.put(commandId, previousWriter);
}
@Override
public void close() {
// Tear down the LogFileHandler state.
removeReferenceCountedWriter(commandId);
// Tear down the ConsoleHandler state.
commandIdToConsoleHandlerWriter.put(
commandId, ConsoleHandler.utf8OutputStreamWriter(consoleHandlerOriginalStream));
commandIdToConsoleHandlerLevel.remove(commandId);
commandIdToIsSuperconsoleEnabled.remove(commandId);
commandIdToIsDaemon.remove(commandId);
// Tear down the shared state.
// NOTE: Avoid iterator in case there's a concurrent change to this map.
List<Long> allKeys = Lists.newArrayList(threadIdToCommandId.keySet());
for (Long threadId1 : allKeys) {
if (commandId.equals(threadIdToCommandId.get(threadId1))) {
threadIdToCommandId.remove(threadId1);
}
}
try {
LOG_FILE_DIR_CLEANER.clean(info.getLogDirectoryPath().getParent());
} catch (IOException e) {
LOG.info(e, "It's possible another concurrent buck command removed the file.");
}
}
};
}
private ReferenceCountedWriter newReferenceCountedWriter(String filePath)
throws FileNotFoundException {
try {
return new ReferenceCountedWriter(
new OutputStreamWriter(new FileOutputStream(filePath), "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private void removeReferenceCountedWriter(String commandId) {
putReferenceCountedWriter(commandId, null);
}
private void putReferenceCountedWriter(
String commandId, @Nullable ReferenceCountedWriter newWriter) {
try {
java.io.Writer oldWriter;
if (newWriter == null) {
oldWriter = commandIdToLogFileHandlerWriter.remove(commandId);
} else {
oldWriter = commandIdToLogFileHandlerWriter.put(commandId, newWriter);
}
if (oldWriter != null) {
oldWriter.close();
}
} catch (IOException e) {
throw new RuntimeException(String.format("Exception closing writer [%s].", commandId), e);
}
}
private ReferenceCountedWriter rotateDefaultLogFileWriter(Path logFilePath) {
try {
Files.createDirectories(logFilePath.getParent());
return newReferenceCountedWriter(logFilePath.toString());
} catch (FileNotFoundException e) {
throw new RuntimeException(String.format("Could not create file [%s].", logFilePath), e);
} catch (IOException e) {
throw new RuntimeException(String.format("Exception wrapping file [%s].", logFilePath), e);
}
}
public CommonThreadFactoryState getThreadToCommandRegister() {
return new CommonThreadFactoryState() {
@Override
public String threadIdToCommandId(long threadId) {
return threadIdToCommandId.get(threadId);
}
@Override
public void register(long threadId, String commandId) {
threadIdToCommandId.put(threadId, commandId);
}
};
}
public ConsoleHandlerState getConsoleHandlerState() {
return new ConsoleHandlerState() {
@Override
public Writer getWriter(String commandId) {
return commandIdToConsoleHandlerWriter.get(commandId);
}
@Override
public Iterable<Writer> getAllAvailableWriters() {
return commandIdToConsoleHandlerWriter.values();
}
@Override
public Level getLogLevel(String commandId) {
return commandIdToConsoleHandlerLevel.get(commandId);
}
@Override
public String threadIdToCommandId(long threadId) {
return threadIdToCommandId.get(threadId);
}
};
}
public ThreadIdToCommandIdMapper getThreadIdToCommandIdMapper() {
return threadIdToCommandId::get;
}
public CommandIdToIsDaemonMapper getCommandIdToIsDaemonMapper() {
return commandIdToIsDaemon::get;
}
public CommandIdToIsSuperConsoleEnabledMapper getCommandIdToIsSuperConsoleEnabledMapper() {
return commandIdToIsSuperconsoleEnabled::get;
}
/**
* Writers obtained by {@link LogFileHandlerState#getWriters} must not be closed! This class
* manages their lifetime.
*/
public LogFileHandlerState getLogFileHandlerState() {
return new LogFileHandlerState() {
@Override
public Iterable<java.io.Writer> getWriters(@Nullable String commandId) {
if (commandId == null) {
return commandIdToLogFileHandlerWriter.values();
}
java.io.Writer writer = commandIdToLogFileHandlerWriter.get(commandId);
if (writer != null) {
return Collections.singleton(writer);
} else {
return commandIdToLogFileHandlerWriter.values();
}
}
@Override
public String threadIdToCommandId(long threadId) {
return threadIdToCommandId.get(threadId);
}
};
}
/**
* Since this is a Singleton class, make sure it cleans after itself once it's GC'ed.
*
* @exception IOException if an I/O error occurs.
*/
@Override
protected void finalize() throws IOException {
// Close off any log file writers that may still be hanging about.
List<String> allKeys = Lists.newArrayList(commandIdToLogFileHandlerWriter.keySet());
for (String commandId : allKeys) {
removeReferenceCountedWriter(commandId);
}
// Close off any console writers that may still be hanging about.
for (ConsoleHandlerState.Writer writer : commandIdToConsoleHandlerWriter.values()) {
try {
writer.close();
} catch (IOException e) {
// Keep going through all the writers even if one fails.
LOG.error(e, "Failed to cleanly close() and OutputStreamWriter.");
}
}
}
public interface LoggerIsMappedToThreadScope extends AutoCloseable {
Closeable setWriter(ConsoleHandlerState.Writer writer);
@Override
void close();
}
}