From 30381dd85a6981a90cbd0c0b0542f3f1f0926f79 Mon Sep 17 00:00:00 2001 From: arostovtsev Date: Sun, 2 Sep 2018 15:26:08 -0700 Subject: [PATCH] Introduce a trivial querier for configured java.util.logging.FileHandler. For use as a backup for the new server_log info option when using the old log handler setup in Bazel - in case we would need to temporarily roll back the migration to SimpleLogHandler. RELNOTES: None. PiperOrigin-RevId: 211288834 --- .../build/lib/util/FileHandlerQuerier.java | 87 +++++++++++++ .../java/com/google/devtools/build/lib/BUILD | 1 + .../lib/util/FileHandlerQuerierTest.java | 121 ++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 src/main/java/com/google/devtools/build/lib/util/FileHandlerQuerier.java create mode 100644 src/test/java/com/google/devtools/build/lib/util/FileHandlerQuerierTest.java diff --git a/src/main/java/com/google/devtools/build/lib/util/FileHandlerQuerier.java b/src/main/java/com/google/devtools/build/lib/util/FileHandlerQuerier.java new file mode 100644 index 00000000000000..ad68c9cc31b491 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/util/FileHandlerQuerier.java @@ -0,0 +1,87 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// 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.google.devtools.build.lib.util; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Supplier; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.logging.FileHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; + +/** + * A {@link LogHandlerQuerier} for working with {@link java.util.logging.FileHandler} instances. + * + *

This querier is intended for situations where the logging handler is configured on the JVM + * command line to be {@link java.util.logging.FileHandler}, but where the code which needs to query + * the handler does not know the handler's class or cannot import it. The command line then should + * in addition specify {@code + * -Dcom.google.devtools.build.lib.util.LogHandlerQuerier.class=com.google.devtools.build.lib.util.FileHandlerQuerier} + * and an instance of FileHandlerQuerier class can then be obtained from {@code + * LogHandlerQuerier.getInstance()}. + * + *

Due to limitations of java.util.logging API, this querier only supports obtaining the log file + * path when it's specified in java.util.logging.config with no % variables. + * + *

TODO: is intended that this class be removed once Bazel is no longer using + * {@link java.util.logging.FileHandler}. + */ +public class FileHandlerQuerier extends LogHandlerQuerier { + /** Wrapper around LogManager.getLogManager() for testing. */ + private final Supplier logManagerSupplier; + + @VisibleForTesting + FileHandlerQuerier(Supplier logManagerSupplier) { + this.logManagerSupplier = logManagerSupplier; + } + + public FileHandlerQuerier() { + this(() -> LogManager.getLogManager()); + } + + @Override + protected boolean canQuery(Handler handler) { + return handler instanceof FileHandler; + } + + @Override + protected Optional getLogHandlerFilePath(Handler handler) { + // Hack: java.util.logging.FileHandler has no API for getting the current file path. Instead, we + // try to parse the configured path and check that it has no % variables. + String pattern = logManagerSupplier.get().getProperty("java.util.logging.FileHandler.pattern"); + if (pattern == null) { + throw new IllegalStateException( + "java.util.logging.config property java.util.logging.FileHandler.pattern is undefined"); + } + if (pattern.matches(".*%[thgu].*")) { + throw new IllegalStateException( + "resolving %-coded variables in java.util.logging.config property " + + "java.util.logging.FileHandler.pattern is not supported"); + } + Path path = Paths.get(pattern.trim()); + + // Hack: java.util.logging.FileHandler has no API for checking if a log file is currently open. + // Instead, we try to query whether the handler can log a SEVERE level record - which for + // expected configurations should be true iff a log file is open. + if (!handler.isLoggable(new LogRecord(Level.SEVERE, ""))) { + return Optional.empty(); + } + return Optional.of(path); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD index 25ce176699e7db..d059b3e97ac180 100644 --- a/src/test/java/com/google/devtools/build/lib/BUILD +++ b/src/test/java/com/google/devtools/build/lib/BUILD @@ -325,6 +325,7 @@ java_test( "//src/main/java/com/google/devtools/build/lib/vfs", "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs", "//src/main/java/com/google/devtools/common/options", + "//third_party:mockito", "//third_party:truth8", ], ) diff --git a/src/test/java/com/google/devtools/build/lib/util/FileHandlerQuerierTest.java b/src/test/java/com/google/devtools/build/lib/util/FileHandlerQuerierTest.java new file mode 100644 index 00000000000000..0647230f63e984 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/util/FileHandlerQuerierTest.java @@ -0,0 +1,121 @@ +// Copyright 2018 The Bazel Authors. All rights reserved. +// +// 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.google.devtools.build.lib.util; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.logging.FileHandler; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +/** Tests for the {@link FileHandlerQuerier} class. */ +@RunWith(JUnit4.class) +public class FileHandlerQuerierTest { + @Rule public TemporaryFolder tmp = new TemporaryFolder(); + + private Logger getLoggerWithFileHandler(FileHandler handler) { + Logger logger = Logger.getAnonymousLogger(); + logger.addHandler(handler); + return logger; + } + + private Logger getLoggerWithFileHandler(Path logPath) throws IOException { + return getLoggerWithFileHandler(new FileHandler(logPath.toString())); + } + + @Test + public void getLoggerFilePath_onExpectedConfigurationOpenFile_returnsPath() throws Exception { + Path configuredLogPath = Paths.get(tmp.getRoot().toString(), "hello.log"); + LogManager mockLogManager = Mockito.mock(LogManager.class); + when(mockLogManager.getProperty("java.util.logging.FileHandler.pattern")) + .thenReturn(configuredLogPath.toString()); + Logger logger = getLoggerWithFileHandler(configuredLogPath); + FileHandlerQuerier handlerQuerier = new FileHandlerQuerier(() -> mockLogManager); + + Optional retrievedLogPath = handlerQuerier.getLoggerFilePath(logger); + + assertThat(retrievedLogPath).isPresent(); + assertThat(retrievedLogPath.get().toString()).isEqualTo(configuredLogPath.toString()); + } + + @Test + public void getLoggerFilePath_onExpectedConfigurationClosedFile_returnsEmpty() throws Exception { + Path configuredLogPath = Paths.get(tmp.getRoot().toString(), "hello.log"); + LogManager mockLogManager = Mockito.mock(LogManager.class); + when(mockLogManager.getProperty("java.util.logging.FileHandler.pattern")) + .thenReturn(configuredLogPath.toString()); + FileHandler handler = new FileHandler(configuredLogPath.toString()); + Logger logger = getLoggerWithFileHandler(handler); + FileHandlerQuerier handlerQuerier = new FileHandlerQuerier(() -> mockLogManager); + handler.close(); + + assertThat(handlerQuerier.getLoggerFilePath(logger)).isEmpty(); + } + + @Test + public void getLoggerFilePath_onMissingConfiguration_fails() throws Exception { + Path configuredLogPath = Paths.get(tmp.getRoot().toString(), "hello.log"); + LogManager mockLogManager = Mockito.mock(LogManager.class); + when(mockLogManager.getProperty("java.util.logging.FileHandler.pattern")).thenReturn(null); + Logger logger = getLoggerWithFileHandler(configuredLogPath); + FileHandlerQuerier handlerQuerier = new FileHandlerQuerier(() -> mockLogManager); + + assertThrows(IllegalStateException.class, () -> handlerQuerier.getLoggerFilePath(logger)); + } + + @Test + public void getLoggerFilePath_onVariablesInPath_fails() throws Exception { + LogManager mockLogManager = Mockito.mock(LogManager.class); + when(mockLogManager.getProperty("java.util.logging.FileHandler.pattern")) + .thenReturn(tmp.getRoot() + File.separator + "hello_%u.log"); + Logger logger = + getLoggerWithFileHandler(Paths.get(tmp.getRoot().toString(), "hello_0.log")); + FileHandlerQuerier handlerQuerier = new FileHandlerQuerier(); + + assertThrows(IllegalStateException.class, () -> handlerQuerier.getLoggerFilePath(logger)); + } + + @Test + public void getLoggerFilePath_onUnsupportedLogHandler_fails() throws Exception { + FileHandlerQuerier handlerQuerier = new FileHandlerQuerier(); + Logger logger = Logger.getAnonymousLogger(); + logger.addHandler( + SimpleLogHandler.builder().setPrefix(tmp.getRoot() + File.separator + "hello.log").build()); + + assertThrows(IllegalArgumentException.class, () -> handlerQuerier.getLoggerFilePath(logger)); + } + + @Test + public void getLoggerFilePath_onMissingLogHandler_fails() throws Exception { + FileHandlerQuerier handlerQuerier = new FileHandlerQuerier(); + Logger logger = Logger.getAnonymousLogger(); + + assertThrows(IllegalArgumentException.class, () -> handlerQuerier.getLoggerFilePath(logger)); + } +}