stream = Files.list(directory)) {
- if (stream.findFirst().isPresent()) {
- return;
- }
- }
-
- Files.delete(directory);
- }
-
- /**
- * Dumps the current execution data, converts it, writes it to the output directory defined in {@link #options} and
- * uploads it if an uploader is configured. Logs any errors, never throws an exception.
- */
- @Override
- public void dumpReport() {
- logger.debug("Starting dump");
-
- try {
- dumpReportUnsafe();
- } catch (Throwable t) {
- // we want to catch anything in order to avoid crashing the whole system under
- // test
- logger.error("Dump job failed with an exception", t);
- }
- }
-
- private void dumpReportUnsafe() {
- Dump dump;
- try {
- dump = controller.dumpAndReset();
- } catch (JacocoRuntimeController.DumpException e) {
- logger.error("Dumping failed, retrying later", e);
- return;
- }
-
- try (Benchmark ignored = new Benchmark("Generating the XML report")) {
- File outputFile = options.createNewFileInOutputDirectory("jacoco", "xml");
- CoverageFile coverageFile = generator.convertSingleDumpToReport(dump, outputFile);
- uploader.upload(coverageFile);
- } catch (IOException e) {
- logger.error("Converting binary dump to XML failed", e);
- } catch (EmptyReportException e) {
- logger.error("No coverage was collected. " + e.getMessage(), e);
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java
deleted file mode 100644
index 13b98f37e..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentBase.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.options.AgentOptions;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.util.thread.QueuedThreadPool;
-import org.glassfish.jersey.server.ResourceConfig;
-import org.glassfish.jersey.servlet.ServletContainer;
-import org.jacoco.agent.rt.RT;
-import org.slf4j.Logger;
-
-import java.lang.management.ManagementFactory;
-
-/**
- * Base class for agent implementations. Handles logger shutdown, store creation and instantiation of the
- * {@link JacocoRuntimeController}.
- *
- * Subclasses must handle dumping onto disk and uploading via the configured uploader.
- */
-public abstract class AgentBase {
-
- /** The logger. */
- protected final Logger logger = LoggingUtils.getLogger(this);
-
- /** Controls the JaCoCo runtime. */
- public final JacocoRuntimeController controller;
-
- /** The agent options. */
- protected AgentOptions options;
-
- private Server server;
-
- /** Constructor. */
- public AgentBase(AgentOptions options) throws IllegalStateException {
- this.options = options;
-
- try {
- controller = new JacocoRuntimeController(RT.getAgent());
- } catch (IllegalStateException e) {
- throw new IllegalStateException(
- "Teamscale Java Profiler not started or there is a conflict with another agent on the classpath.",
- e);
- }
- logger.info("Starting Teamscale Java Profiler for process {} with options: {}",
- ManagementFactory.getRuntimeMXBean().getName(), getOptionsObjectToLog());
- if (options.getHttpServerPort() != null) {
- try {
- initServer();
- } catch (Exception e) {
- logger.error("Could not start http server on port " + options.getHttpServerPort()
- + ". Please check if the port is blocked.");
- throw new IllegalStateException("Control server not started.", e);
- }
- }
- }
-
-
-
- /**
- * Lazily generated string representation of the command line arguments to print to the log.
- */
- private Object getOptionsObjectToLog() {
- return new Object() {
- @Override
- public String toString() {
- if (options.shouldObfuscateSecurityRelatedOutputs()) {
- return options.getObfuscatedOptionsString();
- }
- return options.getOriginalOptionsString();
- }
- };
- }
-
- /**
- * Starts the http server, which waits for information about started and finished tests.
- */
- private void initServer() throws Exception {
- logger.info("Listening for test events on port {}.", options.getHttpServerPort());
-
- // Jersey Implementation
- ServletContextHandler handler = buildUsingResourceConfig();
- QueuedThreadPool threadPool = new QueuedThreadPool();
- threadPool.setMaxThreads(10);
- threadPool.setDaemon(true);
-
- // Create a server instance and set the thread pool
- server = new Server(threadPool);
- // Create a server connector, set the port and add it to the server
- ServerConnector connector = new ServerConnector(server);
- connector.setPort(options.getHttpServerPort());
- server.addConnector(connector);
- server.setHandler(handler);
- server.start();
- }
-
- private ServletContextHandler buildUsingResourceConfig() {
- ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
- handler.setContextPath("/");
-
- ResourceConfig resourceConfig = initResourceConfig();
- handler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/*");
- return handler;
- }
-
- /**
- * Initializes the {@link ResourceConfig} needed for the Jetty + Jersey Server
- */
- protected abstract ResourceConfig initResourceConfig();
-
- /**
- * Registers a shutdown hook that stops the timer and dumps coverage a final time.
- */
- void registerShutdownHook() {
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- try {
- logger.info("Teamscale Java Profiler is shutting down...");
- stopServer();
- prepareShutdown();
- logger.info("Teamscale Java Profiler successfully shut down.");
- } catch (Exception e) {
- logger.error("Exception during profiler shutdown.", e);
- } finally {
- // Try to flush logging resources also in case of an exception during shutdown
- PreMain.closeLoggingResources();
- }
- }));
- }
-
- /** Stop the http server if it's running */
- void stopServer() {
- if (options.getHttpServerPort() != null) {
- try {
- server.stop();
- } catch (Exception e) {
- logger.error("Could not stop server so it is killed now.", e);
- } finally {
- server.destroy();
- }
- }
- }
-
- /** Called when the shutdown hook is triggered. */
- protected void prepareShutdown() {
- // Template method to be overridden by subclasses.
- }
-
- /**
- * Dumps the current execution data, converts it, writes it to the output
- * directory defined in {@link #options} and uploads it if an uploader is
- * configured. Logs any errors, never throws an exception.
- */
- public abstract void dumpReport();
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/AgentResource.java b/agent/src/main/java/com/teamscale/jacoco/agent/AgentResource.java
deleted file mode 100644
index 51684e739..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/AgentResource.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.core.Response;
-
-/**
- * The resource of the Jersey + Jetty http server holding all the endpoints specific for the {@link Agent}.
- */
-@Path("/")
-public class AgentResource extends ResourceBase {
-
- private static Agent agent;
-
- /**
- * Static setter to inject the {@link Agent} to the resource.
- */
- public static void setAgent(Agent agent) {
- AgentResource.agent = agent;
- ResourceBase.agentBase = agent;
- }
-
- /** Handles dumping a XML coverage report for coverage collected until now. */
- @POST
- @Path("/dump")
- public Response handleDump() {
- logger.debug("Dumping report triggered via HTTP request");
- agent.dumpReport();
- return Response.noContent().build();
- }
-
- /** Handles resetting of coverage. */
- @POST
- @Path("/reset")
- public Response handleReset() {
- logger.debug("Resetting coverage triggered via HTTP request");
- agent.controller.reset();
- return Response.noContent().build();
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java b/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java
deleted file mode 100644
index 574389c58..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/DelayedLogger.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.report.util.ILogger;
-import org.slf4j.Logger;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A logger that buffers logs in memory and writes them to the actual logger at a later point. This is needed when stuff
- * needs to be logged before the actual logging framework is initialized.
- */
-public class DelayedLogger implements ILogger {
-
- /** List of log actions that will be executed once the logger is initialized. */
- private final List logActions = new ArrayList<>();
-
- @Override
- public void debug(String message) {
- logActions.add(logger -> logger.debug(message));
- }
-
- @Override
- public void info(String message) {
- logActions.add(logger -> logger.info(message));
- }
-
- @Override
- public void warn(String message) {
- logActions.add(logger -> logger.warn(message));
- }
-
- @Override
- public void warn(String message, Throwable throwable) {
- logActions.add(logger -> logger.warn(message, throwable));
- }
-
- @Override
- public void error(Throwable throwable) {
- logActions.add(logger -> logger.error(throwable.getMessage(), throwable));
- }
-
- @Override
- public void error(String message, Throwable throwable) {
- logActions.add(logger -> logger.error(message, throwable));
- }
-
- /**
- * Logs an error and also writes the message to {@link System#err} to ensure the message is even logged in case
- * setting up the logger itself fails for some reason (see TS-23151).
- */
- public void errorAndStdErr(String message, Throwable throwable) {
- System.err.println(message);
- logActions.add(logger -> logger.error(message, throwable));
- }
-
- /** Writes the logs to the given slf4j logger. */
- public void logTo(Logger logger) {
- logActions.forEach(action -> action.log(logger));
- }
-
- /** An action to be executed on a logger. */
- private interface ILoggerAction {
-
- /** Executes the action on the given logger. */
- void log(Logger logger);
-
- }
-}
-
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/GenericExceptionMapper.java b/agent/src/main/java/com/teamscale/jacoco/agent/GenericExceptionMapper.java
deleted file mode 100644
index 76c1d043f..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/GenericExceptionMapper.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.ext.ExceptionMapper;
-
-/**
- * Generates a {@link Response} for an exception.
- */
-@javax.ws.rs.ext.Provider
-public class GenericExceptionMapper implements ExceptionMapper {
-
- @Override
- public Response toResponse(Throwable e) {
- Response.ResponseBuilder errorResponse = Response.status(Response.Status.INTERNAL_SERVER_ERROR);
- errorResponse.type(MediaType.TEXT_PLAIN_TYPE);
- errorResponse.entity("Message: " + e.getMessage());
- return errorResponse.build();
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/JacocoRuntimeController.java b/agent/src/main/java/com/teamscale/jacoco/agent/JacocoRuntimeController.java
deleted file mode 100644
index 1ad9b2146..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/JacocoRuntimeController.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2018 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.report.jacoco.dump.Dump;
-import org.jacoco.agent.rt.IAgent;
-import org.jacoco.agent.rt.RT;
-import org.jacoco.core.data.ExecutionDataReader;
-import org.jacoco.core.data.ExecutionDataStore;
-import org.jacoco.core.data.ISessionInfoVisitor;
-import org.jacoco.core.data.SessionInfo;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-
-/**
- * Wrapper around JaCoCo's {@link RT} runtime interface.
- *
- * Can be used if the calling code is run in the same JVM as the agent is attached to.
- */
-public class JacocoRuntimeController {
-
- /** Indicates a failed dump. */
- public static class DumpException extends Exception {
-
- /** Serialization ID. */
- private static final long serialVersionUID = 1L;
-
- /** Constructor. */
- public DumpException(String message, Throwable cause) {
- super(message, cause);
- }
-
- }
-
- /** JaCoCo's {@link RT} agent instance */
- private final IAgent agent;
-
- /** Constructor. */
- public JacocoRuntimeController(IAgent agent) {
- this.agent = agent;
- }
-
- /**
- * Dumps execution data and resets it.
- *
- * @throws DumpException if dumping fails. This should never happen in real life. Dumping should simply be retried
- * later if this ever happens.
- */
- public Dump dumpAndReset() throws DumpException {
- byte[] binaryData = agent.getExecutionData(true);
-
- try (ByteArrayInputStream inputStream = new ByteArrayInputStream(binaryData)) {
- ExecutionDataReader reader = new ExecutionDataReader(inputStream);
-
- ExecutionDataStore store = new ExecutionDataStore();
- reader.setExecutionDataVisitor(store::put);
-
- SessionInfoVisitor sessionInfoVisitor = new SessionInfoVisitor();
- reader.setSessionInfoVisitor(sessionInfoVisitor);
-
- reader.read();
- return new Dump(sessionInfoVisitor.sessionInfo, store);
- } catch (IOException e) {
- throw new DumpException("should never happen for the ByteArrayInputStream", e);
- }
- }
-
- /**
- * Dumps execution data to the given file and resets it afterwards.
- */
- public void dumpToFileAndReset(File file) throws IOException {
- byte[] binaryData = agent.getExecutionData(true);
-
- try (FileOutputStream outputStream = new FileOutputStream(file, true)) {
- outputStream.write(binaryData);
- }
- }
-
-
- /**
- * Dumps execution data to a file and resets it.
- *
- * @throws DumpException if dumping fails. This should never happen in real life. Dumping should simply be retried
- * later if this ever happens.
- */
- public void dump() throws DumpException {
- try {
- agent.dump(true);
- } catch (IOException e) {
- throw new DumpException(e.getMessage(), e);
- }
- }
-
- /** Resets already collected coverage. */
- public void reset() {
- agent.reset();
- }
-
- /** Returns the current sessionId. */
- public String getSessionId() {
- return agent.getSessionId();
- }
-
- /**
- * Sets the current sessionId of the agent that can be used to identify which coverage is recorded from now on.
- */
- public void setSessionId(String sessionId) {
- agent.setSessionId(sessionId);
- }
-
- /** Unsets the session ID so that coverage collected from now on is not attributed to the previous test. */
- public void resetSessionId() {
- agent.setSessionId("");
- }
-
- /**
- * Receives and stores a {@link SessionInfo}. Has a fallback dummy session in case nothing is received.
- */
- private static class SessionInfoVisitor implements ISessionInfoVisitor {
-
- /** The received session info or a dummy. */
- public SessionInfo sessionInfo = new SessionInfo("dummysession", System.currentTimeMillis(),
- System.currentTimeMillis());
-
- /** {@inheritDoc} */
- @Override
- public void visitSessionInfo(SessionInfo info) {
- this.sessionInfo = info;
- }
-
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/LenientCoverageTransformer.java b/agent/src/main/java/com/teamscale/jacoco/agent/LenientCoverageTransformer.java
deleted file mode 100644
index 51c65737b..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/LenientCoverageTransformer.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import org.jacoco.agent.rt.internal_29a6edd.CoverageTransformer;
-import org.jacoco.agent.rt.internal_29a6edd.core.runtime.AgentOptions;
-import org.jacoco.agent.rt.internal_29a6edd.core.runtime.IRuntime;
-import org.slf4j.Logger;
-
-import java.lang.instrument.IllegalClassFormatException;
-import java.security.ProtectionDomain;
-
-/**
- * A class file transformer which delegates to the JaCoCo {@link CoverageTransformer} to do the actual instrumentation,
- * but treats instrumentation errors e.g. due to unsupported class file versions more lenient by only logging them, but
- * not bailing out completely. Those unsupported classes will not be instrumented and will therefore not be contained in
- * the collected coverage report.
- */
-public class LenientCoverageTransformer extends CoverageTransformer {
-
- private final Logger logger;
-
- public LenientCoverageTransformer(IRuntime runtime, AgentOptions options, Logger logger) {
- // The coverage transformer only uses the logger to print an error when the instrumentation fails.
- // We want to show our more specific error message instead, so we only log this for debugging at trace.
- super(runtime, options, e -> logger.trace(e.getMessage(), e));
- this.logger = logger;
- }
-
- @Override
- public byte[] transform(ClassLoader loader, String classname, Class> classBeingRedefined,
- ProtectionDomain protectionDomain,
- byte[] classfileBuffer) {
- try {
- return super.transform(loader, classname, classBeingRedefined, protectionDomain, classfileBuffer);
- } catch (IllegalClassFormatException e) {
- logger.error(
- "Failed to instrument " + classname + ". File will be skipped from instrumentation. " +
- "No coverage will be collected for it. Exclude the file from the instrumentation or try " +
- "updating the Teamscale Java Profiler if the file should actually be instrumented. (Cause: {})",
- getRootCauseMessage(e));
- return null;
- }
- }
-
- private static String getRootCauseMessage(Throwable e) {
- if (e.getCause() != null) {
- return getRootCauseMessage(e.getCause());
- }
- return e.getMessage();
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/Main.java b/agent/src/main/java/com/teamscale/jacoco/agent/Main.java
deleted file mode 100644
index 20c92dae8..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/Main.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.JCommander.Builder;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.commandline.Validator;
-import com.teamscale.jacoco.agent.convert.ConvertCommand;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.util.AgentUtils;
-import org.jacoco.core.JaCoCo;
-import org.slf4j.Logger;
-
-/** Provides a command line interface for interacting with JaCoCo. */
-public class Main {
-
- /** The logger. */
- private final Logger logger = LoggingUtils.getLogger(this);
-
- /** The default arguments that will always be parsed. */
- private final DefaultArguments defaultArguments = new DefaultArguments();
-
- /** The arguments for the one-time conversion process. */
- private final ConvertCommand command = new ConvertCommand();
-
- /** Entry point. */
- public static void main(String[] args) throws Exception {
- new Main().parseCommandLineAndRun(args);
- }
-
- /**
- * Parses the given command line arguments. Exits the program or throws an exception if the arguments are not valid.
- * Then runs the specified command.
- */
- private void parseCommandLineAndRun(String[] args) throws Exception {
- Builder builder = createJCommanderBuilder();
- JCommander jCommander = builder.build();
-
- try {
- jCommander.parse(args);
- } catch (ParameterException e) {
- handleInvalidCommandLine(jCommander, e.getMessage());
- }
-
- if (defaultArguments.help) {
- System.out.println(
- "Teamscale Java Profiler " + AgentUtils.VERSION + " compiled against JaCoCo " + JaCoCo.VERSION);
- jCommander.usage();
- return;
- }
-
- Validator validator = command.validate();
- if (!validator.isValid()) {
- handleInvalidCommandLine(jCommander, StringUtils.LINE_FEED + validator.getErrorMessage());
- }
-
- logger.info(
- "Starting Teamscale Java Profiler " + AgentUtils.VERSION + " compiled against JaCoCo " + JaCoCo.VERSION);
- command.run();
- }
-
- /** Creates a builder for a {@link JCommander} object. */
- private Builder createJCommanderBuilder() {
- return JCommander.newBuilder().programName(Main.class.getName()).addObject(defaultArguments).addObject(command);
- }
-
- /** Shows an informative error and help message. Then exits the program. */
- private static void handleInvalidCommandLine(JCommander jCommander, String message) {
- System.err.println("Invalid command line: " + message + StringUtils.LINE_FEED);
- jCommander.usage();
- System.exit(1);
- }
-
- /** Default arguments that may always be provided. */
- private static class DefaultArguments {
-
- /** Shows the help message. */
- @Parameter(names = "--help", help = true, description = "Shows all available command line arguments.")
- private boolean help;
-
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java b/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java
deleted file mode 100644
index 921a115b8..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/PreMain.java
+++ /dev/null
@@ -1,305 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.HttpUtils;
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.configuration.AgentOptionReceiveException;
-import com.teamscale.jacoco.agent.logging.DebugLogDirectoryPropertyDefiner;
-import com.teamscale.jacoco.agent.logging.LogDirectoryPropertyDefiner;
-import com.teamscale.jacoco.agent.logging.LogToTeamscaleAppender;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.options.AgentOptionParseException;
-import com.teamscale.jacoco.agent.options.AgentOptions;
-import com.teamscale.jacoco.agent.options.AgentOptionsParser;
-import com.teamscale.jacoco.agent.options.FilePatternResolver;
-import com.teamscale.jacoco.agent.options.JacocoAgentOptionsBuilder;
-import com.teamscale.jacoco.agent.options.TeamscaleCredentials;
-import com.teamscale.jacoco.agent.options.TeamscalePropertiesUtils;
-import com.teamscale.jacoco.agent.testimpact.TestwiseCoverageAgent;
-import com.teamscale.jacoco.agent.upload.UploaderException;
-import com.teamscale.jacoco.agent.util.AgentUtils;
-import com.teamscale.report.util.ILogger;
-import kotlin.Pair;
-import org.slf4j.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.instrument.Instrumentation;
-import java.lang.management.ManagementFactory;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import static com.teamscale.jacoco.agent.logging.LoggingUtils.getLoggerContext;
-
-/** Container class for the premain entry point for the agent. */
-public class PreMain {
-
- private static LoggingUtils.LoggingResources loggingResources = null;
-
- /**
- * System property that we use to prevent this agent from being attached to the same VM twice. This can happen if
- * the agent is registered via multiple JVM environment variables and/or the command line at the same time.
- */
- private static final String LOCKING_SYSTEM_PROPERTY = "TEAMSCALE_JAVA_PROFILER_ATTACHED";
-
- /**
- * Environment variable from which to read the config ID to use. This is an ID for a profiler configuration that is
- * stored in Teamscale.
- */
- private static final String CONFIG_ID_ENVIRONMENT_VARIABLE = "TEAMSCALE_JAVA_PROFILER_CONFIG_ID";
-
- /** Environment variable from which to read the config file to use. */
- private static final String CONFIG_FILE_ENVIRONMENT_VARIABLE = "TEAMSCALE_JAVA_PROFILER_CONFIG_FILE";
-
- /** Environment variable from which to read the Teamscale access token. */
- private static final String ACCESS_TOKEN_ENVIRONMENT_VARIABLE = "TEAMSCALE_ACCESS_TOKEN";
-
- /**
- * Entry point for the agent, called by the JVM.
- */
- public static void premain(String options, Instrumentation instrumentation) throws Exception {
- if (System.getProperty(LOCKING_SYSTEM_PROPERTY) != null) {
- return;
- }
- System.setProperty(LOCKING_SYSTEM_PROPERTY, "true");
-
- String environmentConfigId = System.getenv(CONFIG_ID_ENVIRONMENT_VARIABLE);
- String environmentConfigFile = System.getenv(CONFIG_FILE_ENVIRONMENT_VARIABLE);
- if (StringUtils.isEmpty(options) && environmentConfigId == null && environmentConfigFile == null) {
- // profiler was registered globally and no config was set explicitly by the user, thus ignore this process
- // and don't profile anything
- return;
- }
-
- AgentOptions agentOptions = null;
- try {
- Pair> parseResult = getAndApplyAgentOptions(options, environmentConfigId,
- environmentConfigFile);
- agentOptions = parseResult.getFirst();
-
- // After parsing everything and configuring logging, we now
- // can throw the caught exceptions.
- for (Exception exception : parseResult.getSecond()) {
- throw exception;
- }
- } catch (AgentOptionParseException e) {
- getLoggerContext().getLogger(PreMain.class).error(e.getMessage(), e);
-
- // Flush logs to Teamscale, if configured.
- closeLoggingResources();
-
- // Unregister the profiler from Teamscale.
- if (agentOptions != null && agentOptions.configurationViaTeamscale != null) {
- agentOptions.configurationViaTeamscale.unregisterProfiler();
- }
-
- throw e;
- } catch (AgentOptionReceiveException e) {
- // When Teamscale is not available, we don't want to fail hard to still allow for testing even if no
- // coverage is collected (see TS-33237)
- return;
- }
-
- Logger logger = LoggingUtils.getLogger(Agent.class);
-
- logger.info("Teamscale Java profiler version " + AgentUtils.VERSION);
- logger.info("Starting JaCoCo's agent");
- JacocoAgentOptionsBuilder agentBuilder = new JacocoAgentOptionsBuilder(agentOptions);
- JaCoCoPreMain.premain(agentBuilder.createJacocoAgentOptions(), instrumentation, logger);
-
- if (agentOptions.configurationViaTeamscale != null) {
- agentOptions.configurationViaTeamscale.startHeartbeatThreadAndRegisterShutdownHook();
- }
- AgentBase agent = createAgent(agentOptions, instrumentation);
- agent.registerShutdownHook();
- }
-
- private static Pair> getAndApplyAgentOptions(String options,
- String environmentConfigId,
- String environmentConfigFile) throws AgentOptionParseException, IOException, AgentOptionReceiveException {
-
- DelayedLogger delayedLogger = new DelayedLogger();
- List javaAgents = ManagementFactory.getRuntimeMXBean().getInputArguments().stream().filter(
- s -> s.contains("-javaagent")).collect(Collectors.toList());
- // We allow multiple instances of the teamscale-jacoco-agent as we ensure with the #LOCKING_SYSTEM_PROPERTY to only use it once
- List differentAgents = javaAgents.stream()
- .filter(javaAgent -> !javaAgent.contains("teamscale-jacoco-agent.jar")).collect(
- Collectors.toList());
-
- if (!differentAgents.isEmpty()) {
- delayedLogger.warn(
- "Using multiple java agents could interfere with coverage recording: " +
- String.join(", ", differentAgents));
- }
- if (!javaAgents.get(0).contains("teamscale-jacoco-agent.jar")) {
- delayedLogger.warn("For best results consider registering the Teamscale Java Profiler first.");
- }
-
- TeamscaleCredentials credentials = TeamscalePropertiesUtils.parseCredentials();
- if (credentials == null) {
- // As many users still don't use the installer based setup, this log message will be shown in almost every log.
- // We use a debug log, as this message can be confusing for customers that think a teamscale.properties file is synonymous with a config file.
- delayedLogger.debug(
- "No explicit teamscale.properties file given. Looking for Teamscale credentials in a config file or via a command line argument. This is expected unless the installer based setup was used.");
- }
-
- String environmentAccessToken = System.getenv(ACCESS_TOKEN_ENVIRONMENT_VARIABLE);
-
- Pair> parseResult;
- AgentOptions agentOptions;
- try {
- parseResult = AgentOptionsParser.parse(
- options, environmentConfigId, environmentConfigFile, credentials, environmentAccessToken,
- delayedLogger);
- agentOptions = parseResult.getFirst();
- } catch (AgentOptionParseException e) {
- try (LoggingUtils.LoggingResources ignored = initializeFallbackLogging(options, delayedLogger)) {
- delayedLogger.errorAndStdErr("Failed to parse agent options: " + e.getMessage(), e);
- attemptLogAndThrow(delayedLogger);
- throw e;
- }
- } catch (AgentOptionReceiveException e) {
- try (LoggingUtils.LoggingResources ignored = initializeFallbackLogging(options, delayedLogger)) {
- delayedLogger.errorAndStdErr(
- e.getMessage() + " The application should start up normally, but NO coverage will be collected! Check the log file for details.",
- e);
- attemptLogAndThrow(delayedLogger);
- throw e;
- }
- }
-
- initializeLogging(agentOptions, delayedLogger);
- Logger logger = LoggingUtils.getLogger(Agent.class);
- delayedLogger.logTo(logger);
- HttpUtils.setShouldValidateSsl(agentOptions.shouldValidateSsl());
-
- return parseResult;
- }
-
- private static void attemptLogAndThrow(DelayedLogger delayedLogger) {
- // We perform actual logging output after writing to console to
- // ensure the console is reached even in case of logging issues
- // (see TS-23151). We use the Agent class here (same as below)
- Logger logger = LoggingUtils.getLogger(Agent.class);
- delayedLogger.logTo(logger);
- }
-
- /** Initializes logging during {@link #premain(String, Instrumentation)} and also logs the log directory. */
- private static void initializeLogging(AgentOptions agentOptions, DelayedLogger logger) throws IOException {
- if (agentOptions.isDebugLogging()) {
- initializeDebugLogging(agentOptions, logger);
- } else {
- loggingResources = LoggingUtils.initializeLogging(agentOptions.getLoggingConfig());
- logger.info("Logging to " + new LogDirectoryPropertyDefiner().getPropertyValue());
- }
-
- if (agentOptions.getTeamscaleServerOptions().isConfiguredForServerConnection()) {
- if (LogToTeamscaleAppender.addTeamscaleAppenderTo(getLoggerContext(), agentOptions)) {
- logger.info("Logs are being forwarded to Teamscale at " + agentOptions.getTeamscaleServerOptions().url);
- }
- }
- }
-
- /** Closes the opened logging contexts. */
- static void closeLoggingResources() {
- loggingResources.close();
- }
-
- /**
- * Returns in instance of the agent that was configured. Either an agent with interval based line-coverage dump or
- * the HTTP server is used.
- */
- private static AgentBase createAgent(AgentOptions agentOptions,
- Instrumentation instrumentation) throws UploaderException, IOException {
- if (agentOptions.useTestwiseCoverageMode()) {
- return TestwiseCoverageAgent.create(agentOptions);
- } else {
- return new Agent(agentOptions, instrumentation);
- }
- }
-
- /**
- * Initializes debug logging during {@link #premain(String, Instrumentation)} and also logs the log directory if
- * given.
- */
- private static void initializeDebugLogging(AgentOptions agentOptions, DelayedLogger logger) {
- loggingResources = LoggingUtils.initializeDebugLogging(agentOptions.getDebugLogDirectory());
- Path logDirectory = Paths.get(new DebugLogDirectoryPropertyDefiner().getPropertyValue());
- if (FileSystemUtils.isValidPath(logDirectory.toString()) && Files.isWritable(logDirectory)) {
- logger.info("Logging to " + logDirectory);
- } else {
- logger.warn("Could not create " + logDirectory + ". Logging to console only.");
- }
- }
-
- /**
- * Initializes fallback logging in case of an error during the parsing of the options to
- * {@link #premain(String, Instrumentation)} (see TS-23151). This tries to extract the logging configuration and use
- * this and falls back to the default logger.
- */
- private static LoggingUtils.LoggingResources initializeFallbackLogging(String premainOptions,
- DelayedLogger delayedLogger) {
- if (premainOptions == null) {
- return LoggingUtils.initializeDefaultLogging();
- }
- for (String optionPart : premainOptions.split(",")) {
- if (optionPart.startsWith(AgentOptionsParser.DEBUG + "=")) {
- String value = optionPart.split("=", 2)[1];
- boolean debugDisabled = value.equalsIgnoreCase("false");
- boolean debugEnabled = value.equalsIgnoreCase("true");
- if (debugDisabled) {
- continue;
- }
- Path debugLogDirectory = null;
- if (!value.isEmpty() && !debugEnabled) {
- debugLogDirectory = Paths.get(value);
- }
- return LoggingUtils.initializeDebugLogging(debugLogDirectory);
- }
- if (optionPart.startsWith(AgentOptionsParser.LOGGING_CONFIG_OPTION + "=")) {
- return createFallbackLoggerFromConfig(optionPart.split("=", 2)[1], delayedLogger);
- }
-
- if (optionPart.startsWith(AgentOptionsParser.CONFIG_FILE_OPTION + "=")) {
- String configFileValue = optionPart.split("=", 2)[1];
- Optional loggingConfigLine = Optional.empty();
- try {
- File configFile = new FilePatternResolver(delayedLogger).parsePath(
- AgentOptionsParser.CONFIG_FILE_OPTION, configFileValue).toFile();
- loggingConfigLine = FileSystemUtils.readLinesUTF8(configFile).stream()
- .filter(line -> line.startsWith(AgentOptionsParser.LOGGING_CONFIG_OPTION + "="))
- .findFirst();
- } catch (IOException e) {
- delayedLogger.error("Failed to load configuration from " + configFileValue + ": " + e.getMessage(),
- e);
- }
- if (loggingConfigLine.isPresent()) {
- return createFallbackLoggerFromConfig(loggingConfigLine.get().split("=", 2)[1], delayedLogger);
- }
- }
- }
-
- return LoggingUtils.initializeDefaultLogging();
- }
-
- /** Creates a fallback logger using the given config file. */
- private static LoggingUtils.LoggingResources createFallbackLoggerFromConfig(String configLocation,
- ILogger delayedLogger) {
- try {
- return LoggingUtils.initializeLogging(
- new FilePatternResolver(delayedLogger).parsePath(AgentOptionsParser.LOGGING_CONFIG_OPTION,
- configLocation));
- } catch (IOException e) {
- String message = "Failed to load log configuration from location " + configLocation + ": " + e.getMessage();
- delayedLogger.error(message, e);
- // output the message to console as well, as this might
- // otherwise not make it to the user
- System.err.println(message);
- return LoggingUtils.initializeDefaultLogging();
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/ResourceBase.java b/agent/src/main/java/com/teamscale/jacoco/agent/ResourceBase.java
deleted file mode 100644
index fb539824e..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/ResourceBase.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package com.teamscale.jacoco.agent;
-
-import com.teamscale.client.CommitDescriptor;
-import com.teamscale.client.StringUtils;
-import com.teamscale.client.TeamscaleServer;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.testimpact.TestwiseCoverageAgent;
-import com.teamscale.report.testwise.model.RevisionInfo;
-import org.jetbrains.annotations.Contract;
-import org.slf4j.Logger;
-
-import javax.ws.rs.BadRequestException;
-import javax.ws.rs.GET;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import java.util.Optional;
-
-
-/**
- * The resource of the Jersey + Jetty http server holding all the endpoints specific for the {@link AgentBase}.
- */
-public abstract class ResourceBase {
-
- /** The logger. */
- protected final Logger logger = LoggingUtils.getLogger(this);
-
- /**
- * The agentBase inject via {@link AgentResource#setAgent(Agent)} or
- * {@link com.teamscale.jacoco.agent.testimpact.TestwiseCoverageResource#setAgent(TestwiseCoverageAgent)}.
- */
- protected static AgentBase agentBase;
-
- /** Returns the partition for the Teamscale upload. */
- @GET
- @Path("/partition")
- public String getPartition() {
- return Optional.ofNullable(agentBase.options.getTeamscaleServerOptions().partition).orElse("");
- }
-
- /** Returns the upload message for the Teamscale upload. */
- @GET
- @Path("/message")
- public String getMessage() {
- return Optional.ofNullable(agentBase.options.getTeamscaleServerOptions().getMessage())
- .orElse("");
- }
-
- /** Returns revision information for the Teamscale upload. */
- @GET
- @Path("/revision")
- @Produces(MediaType.APPLICATION_JSON)
- public RevisionInfo getRevision() {
- return this.getRevisionInfo();
- }
-
- /** Returns revision information for the Teamscale upload. */
- @GET
- @Path("/commit")
- @Produces(MediaType.APPLICATION_JSON)
- public RevisionInfo getCommit() {
- return this.getRevisionInfo();
- }
-
- /** Handles setting the partition name. */
- @PUT
- @Path("/partition")
- public Response setPartition(String partitionString) {
- String partition = StringUtils.removeDoubleQuotes(partitionString);
- if (partition == null || partition.isEmpty()) {
- handleBadRequest("The new partition name is missing in the request body! Please add it as plain text.");
- }
-
- logger.debug("Changing partition name to " + partition);
- agentBase.dumpReport();
- agentBase.controller.setSessionId(partition);
- agentBase.options.getTeamscaleServerOptions().partition = partition;
- return Response.noContent().build();
- }
-
- /** Handles setting the upload message. */
- @PUT
- @Path("/message")
- public Response setMessage(String messageString) {
- String message = StringUtils.removeDoubleQuotes(messageString);
- if (message == null || message.isEmpty()) {
- handleBadRequest("The new message is missing in the request body! Please add it as plain text.");
- }
-
- agentBase.dumpReport();
- logger.debug("Changing message to " + message);
- agentBase.options.getTeamscaleServerOptions().setMessage(message);
-
- return Response.noContent().build();
- }
-
- /** Handles setting the revision. */
- @PUT
- @Path("/revision")
- public Response setRevision(String revisionString) {
- String revision = StringUtils.removeDoubleQuotes(revisionString);
- if (revision == null || revision.isEmpty()) {
- handleBadRequest("The new revision name is missing in the request body! Please add it as plain text.");
- }
-
- agentBase.dumpReport();
- logger.debug("Changing revision name to " + revision);
- agentBase.options.getTeamscaleServerOptions().revision = revision;
-
- return Response.noContent().build();
- }
-
- /** Handles setting the upload commit. */
- @PUT
- @Path("/commit")
- public Response setCommit(String commitString) {
- String commit = StringUtils.removeDoubleQuotes(commitString);
- if (commit == null || commit.isEmpty()) {
- handleBadRequest("The new upload commit is missing in the request body! Please add it as plain text.");
- }
-
- agentBase.dumpReport();
- agentBase.options.getTeamscaleServerOptions().commit = CommitDescriptor.parse(commit);
-
- return Response.noContent().build();
- }
-
- /** Returns revision information for the Teamscale upload. */
- private RevisionInfo getRevisionInfo() {
- TeamscaleServer server = agentBase.options.getTeamscaleServerOptions();
- return new RevisionInfo(server.commit, server.revision);
- }
-
- /**
- * Handles bad requests to the endpoints.
- */
- @Contract(value = "_ -> fail")
- protected void handleBadRequest(String message) throws BadRequestException {
- logger.error(message);
- throw new BadRequestException(message);
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commandline/Validator.java b/agent/src/main/java/com/teamscale/jacoco/agent/commandline/Validator.java
deleted file mode 100644
index 40b7ce388..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commandline/Validator.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2017 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.commandline;
-
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.util.Assertions;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Helper class to allow for multiple validations to occur.
- */
-public class Validator {
-
- /** The found validation problems in the form of error messages for the user. */
- private final List messages = new ArrayList<>();
-
- /** Runs the given validation routine. */
- public void ensure(ExceptionBasedValidation validation) {
- try {
- validation.validate();
- } catch (Exception | AssertionError e) {
- messages.add(e.getMessage());
- }
- }
-
- /**
- * Interface for a validation routine that throws an exception when it fails.
- */
- @FunctionalInterface
- public interface ExceptionBasedValidation {
-
- /**
- * Throws an {@link Exception} or {@link AssertionError} if the validation fails.
- */
- void validate() throws Exception, AssertionError;
-
- }
-
- /**
- * Checks that the given condition is true or adds the given error message.
- */
- public void isTrue(boolean condition, String message) {
- ensure(() -> Assertions.isTrue(condition, message));
- }
-
- /**
- * Checks that the given condition is false or adds the given error message.
- */
- public void isFalse(boolean condition, String message) {
- ensure(() -> Assertions.isFalse(condition, message));
- }
-
- /** Returns true if the validation succeeded. */
- public boolean isValid() {
- return messages.isEmpty();
- }
-
- /** Returns an error message with all validation problems that were found. */
- public String getErrorMessage() {
- return "- " + String.join(StringUtils.LINE_FEED + "- ", messages);
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.java
deleted file mode 100644
index 80f9bb19d..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.teamscale.jacoco.agent.configuration;
-
-/** Thrown when retrieving the profiler configuration from Teamscale fails. */
-public class AgentOptionReceiveException extends Exception {
-
- /**
- * Serialization ID.
- */
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructor.
- */
- public AgentOptionReceiveException(String message) {
- super(message);
- }
-
- /**
- * Constructor.
- */
- public AgentOptionReceiveException(String message, Throwable cause) {
- super(message, cause);
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java
deleted file mode 100644
index d4e59a7e6..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java
+++ /dev/null
@@ -1,168 +0,0 @@
-package com.teamscale.jacoco.agent.configuration;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.teamscale.client.ITeamscaleService;
-import com.teamscale.client.JsonUtils;
-import com.teamscale.client.ProcessInformation;
-import com.teamscale.client.ProfilerConfiguration;
-import com.teamscale.client.ProfilerInfo;
-import com.teamscale.client.ProfilerRegistration;
-import com.teamscale.client.TeamscaleServiceGenerator;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.util.AgentUtils;
-import com.teamscale.report.util.ILogger;
-import okhttp3.HttpUrl;
-import okhttp3.ResponseBody;
-import org.jetbrains.annotations.NotNull;
-import retrofit2.Response;
-
-import java.io.IOException;
-import java.time.Duration;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Responsible for holding the configuration that was retrieved from Teamscale and sending regular heartbeat events to
- * keep the profiler information in Teamscale up to date.
- */
-public class ConfigurationViaTeamscale {
-
- /**
- * Two minute timeout. This is quite high to account for an eventual high load on the Teamscale server. This is a
- * tradeoff between fast application startup and potentially missing test coverage.
- */
- private static final Duration LONG_TIMEOUT = Duration.ofMinutes(2);
-
- /**
- * The UUID that Teamscale assigned to this instance of the profiler during the registration. This ID needs to be
- * used when communicating with Teamscale.
- */
- private final String profilerId;
-
- private final ITeamscaleService teamscaleClient;
- private final ProfilerInfo profilerInfo;
-
- public ConfigurationViaTeamscale(ITeamscaleService teamscaleClient, ProfilerRegistration profilerRegistration,
- ProcessInformation processInformation) {
- this.teamscaleClient = teamscaleClient;
- this.profilerId = profilerRegistration.profilerId;
- this.profilerInfo = new ProfilerInfo(processInformation, profilerRegistration.profilerConfiguration);
- }
-
- /**
- * Tries to retrieve the profiler configuration from Teamscale. In case retrieval fails the method throws a
- * {@link AgentOptionReceiveException}.
- */
- public static @NotNull ConfigurationViaTeamscale retrieve(ILogger logger, String configurationId, HttpUrl url,
- String userName,
- String userAccessToken) throws AgentOptionReceiveException {
- ITeamscaleService teamscaleClient = TeamscaleServiceGenerator
- .createService(ITeamscaleService.class, url, userName, userAccessToken, AgentUtils.USER_AGENT,
- LONG_TIMEOUT, LONG_TIMEOUT);
- try {
- ProcessInformation processInformation = new ProcessInformationRetriever(logger).getProcessInformation();
- Response response = teamscaleClient.registerProfiler(configurationId,
- processInformation).execute();
- if (!response.isSuccessful()) {
- throw new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to failed request. Http status: " + response.code()
- + " Body: " + response.errorBody().string());
- }
-
- ResponseBody body = response.body();
- return parseProfilerRegistration(body, response, teamscaleClient, processInformation);
- } catch (IOException e) {
- // we include the causing error message in this exception's message since this causes it to be printed
- // to stderr which is much more helpful than just saying "something didn't work"
- throw new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to network error: " + LoggingUtils.getStackTraceAsString(
- e),
- e);
- }
- }
-
- private static @NotNull ConfigurationViaTeamscale parseProfilerRegistration(ResponseBody body,
- Response response, ITeamscaleService teamscaleClient,
- ProcessInformation processInformation) throws AgentOptionReceiveException, IOException {
- if (body == null) {
- throw new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to empty response. HTTP code: " + response.code());
- }
- // We may only call this once
- String bodyString = body.string();
- try {
- ProfilerRegistration registration = JsonUtils.deserialize(bodyString,
- ProfilerRegistration.class);
- if (registration == null) {
- throw new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to invalid JSON. HTTP code: " + response.code() + " Response: " + bodyString);
- }
- return new ConfigurationViaTeamscale(teamscaleClient, registration, processInformation);
- } catch (JsonProcessingException e) {
- throw new AgentOptionReceiveException(
- "Failed to retrieve profiler configuration from Teamscale due to invalid JSON. HTTP code: " + response.code() + " Response: " + bodyString,
- e);
- }
- }
-
- /** Returns the profiler configuration that was retrieved from Teamscale. */
- public ProfilerConfiguration getProfilerConfiguration() {
- return profilerInfo.profilerConfiguration;
- }
-
-
- /**
- * Starts a heartbeat thread and registers a shutdown hook.
- *
- * This spawns a new thread every minute which sends a heartbeat to Teamscale. It also registers a shutdown hook
- * that unregisters the profiler from Teamscale.
- */
- public void startHeartbeatThreadAndRegisterShutdownHook() {
- ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(runnable -> {
- Thread thread = new Thread(runnable);
- thread.setDaemon(true);
- return thread;
- });
-
- executor.scheduleAtFixedRate(this::sendHeartbeat, 1, 1, TimeUnit.MINUTES);
-
- Runtime.getRuntime().addShutdownHook(new Thread(() -> {
- executor.shutdownNow();
- unregisterProfiler();
- }));
- }
-
- private void sendHeartbeat() {
- try {
- Response response = teamscaleClient.sendHeartbeat(profilerId, profilerInfo).execute();
- if (!response.isSuccessful()) {
- LoggingUtils.getLogger(this)
- .error("Failed to send heartbeat. Teamscale responded with: " + response.errorBody().string());
- }
- } catch (IOException e) {
- LoggingUtils.getLogger(this).error("Failed to send heartbeat to Teamscale!", e);
- }
- }
-
- /** Unregisters the profiler in Teamscale (marks it as shut down). */
- public void unregisterProfiler() {
- try {
- Response response = teamscaleClient.unregisterProfiler(profilerId).execute();
- if (response.code() == 405) {
- response = teamscaleClient.unregisterProfilerLegacy(profilerId).execute();
- }
- if (!response.isSuccessful()) {
- LoggingUtils.getLogger(this)
- .error("Failed to unregister profiler. Teamscale responded with: " + response.errorBody()
- .string());
- }
- } catch (IOException e) {
- LoggingUtils.getLogger(this).error("Failed to unregister profiler!", e);
- }
- }
-
- public String getProfilerId() {
- return profilerId;
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.java
deleted file mode 100644
index 63a34f4cf..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.teamscale.jacoco.agent.configuration;
-
-import com.teamscale.client.ProcessInformation;
-import com.teamscale.report.util.ILogger;
-
-import java.lang.management.ManagementFactory;
-import java.lang.reflect.InvocationTargetException;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-/**
- * Is responsible for retrieving process information such as the host name and process ID.
- */
-public class ProcessInformationRetriever {
-
- private final ILogger logger;
-
- public ProcessInformationRetriever(ILogger logger) {
- this.logger = logger;
- }
-
- /**
- * Retrieves the process information, including the host name and process ID.
- */
- public ProcessInformation getProcessInformation() {
- String hostName = getHostName();
- String processId = getPID();
- return new ProcessInformation(hostName, processId, System.currentTimeMillis());
- }
-
- /**
- * Retrieves the host name of the local machine.
- */
- private String getHostName() {
- try {
- InetAddress inetAddress = InetAddress.getLocalHost();
- return inetAddress.getHostName();
- } catch (UnknownHostException e) {
- logger.error("Failed to determine hostname!", e);
- return "";
- }
- }
-
- /**
- * Returns a string that probably contains the PID.
- *
- * On Java 9 there is an API to get the PID. But since we support Java 8, we may fall back to an undocumented API
- * that at least contains the PID in most JVMs.
- *
- * See This
- * StackOverflow question
- */
- public static String getPID() {
- try {
- Class> processHandleClass = Class.forName("java.lang.ProcessHandle");
- Object processHandle = processHandleClass.getMethod("current").invoke(null);
- Long pid = (Long) processHandleClass.getMethod("pid").invoke(processHandle);
- return pid.toString();
- } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |
- InvocationTargetException e) {
- return ManagementFactory.getRuntimeMXBean().getName();
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/convert/ConvertCommand.java b/agent/src/main/java/com/teamscale/jacoco/agent/convert/ConvertCommand.java
deleted file mode 100644
index 116c5fe44..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/convert/ConvertCommand.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2017 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.convert;
-
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.Parameters;
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.StringUtils;
-import com.teamscale.jacoco.agent.commandline.ICommand;
-import com.teamscale.jacoco.agent.commandline.Validator;
-import com.teamscale.jacoco.agent.options.ClasspathUtils;
-import com.teamscale.jacoco.agent.options.FilePatternResolver;
-import com.teamscale.jacoco.agent.util.Assertions;
-import com.teamscale.report.EDuplicateClassFileBehavior;
-import com.teamscale.report.util.CommandLineLogger;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * Encapsulates all command line options for the convert command for parsing with {@link JCommander}.
- */
-@Parameters(commandNames = "convert", commandDescription = "Converts a binary .exec coverage file to XML. " +
- "Note that the XML report will only contain source file coverage information, but no class coverage.")
-public class ConvertCommand implements ICommand {
-
- /** The directories and/or zips that contain all class files being profiled. */
- @Parameter(names = {"--class-dir", "--jar", "-c"}, required = true, description = ""
- + "The directories or zip/ear/jar/war/... files that contain the compiled Java classes being profiled."
- + " Searches recursively, including inside zips. You may also supply a *.txt file with one path per line.")
- /* package */ List classDirectoriesOrZips = new ArrayList<>();
-
- /**
- * Wildcard include patterns to apply during JaCoCo's traversal of class files.
- */
- @Parameter(names = {"--includes"}, description = ""
- + "Wildcard include patterns to apply to all found class file locations during JaCoCo's traversal of class files."
- + " Note that zip contents are separated from zip files with @ and that you can filter only"
- + " class files, not intermediate folders/zips. Use with great care as missing class files"
- + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered."
- + " Defaults to no filtering. Excludes overrule includes.")
- /* package */ List locationIncludeFilters = new ArrayList<>();
-
- /**
- * Wildcard exclude patterns to apply during JaCoCo's traversal of class files.
- */
- @Parameter(names = {"--excludes", "-e"}, description = ""
- + "Wildcard exclude patterns to apply to all found class file locations during JaCoCo's traversal of class files."
- + " Note that zip contents are separated from zip files with @ and that you can filter only"
- + " class files, not intermediate folders/zips. Use with great care as missing class files"
- + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered."
- + " Defaults to no filtering. Excludes overrule includes.")
- /* package */ List locationExcludeFilters = new ArrayList<>();
-
- /** The directory to write the XML traces to. */
- @Parameter(names = {"--in", "-i"}, required = true, description = "" + "The binary .exec file(s), test details and " +
- "test executions to read. Can be a single file or a directory that is recursively scanned for relevant files.")
- /* package */ List inputFiles = new ArrayList<>();
-
- /** The directory to write the XML traces to. */
- @Parameter(names = {"--out", "-o"}, required = true, description = ""
- + "The file to write the generated XML report to.")
- /* package */ String outputFile = "";
-
- /** Whether to ignore duplicate, non-identical class files. */
- @Parameter(names = {"--duplicates", "-d"}, arity = 1, description = ""
- + "Whether to ignore duplicate, non-identical class files."
- + " This is discouraged and may result in incorrect coverage files. Defaults to WARN. " +
- "Options are FAIL, WARN and IGNORE.")
- /* package */ EDuplicateClassFileBehavior duplicateClassFileBehavior = EDuplicateClassFileBehavior.WARN;
-
- /** Whether to ignore uncovered class files. */
- @Parameter(names = {"--ignore-uncovered-classes"}, required = false, arity = 1, description = ""
- + "Whether to ignore uncovered classes."
- + " These classes will not be part of the XML report at all, making it considerably smaller in some cases. Defaults to false.")
- /* package */ boolean shouldIgnoreUncoveredClasses = false;
-
- /** Whether testwise coverage or jacoco coverage should be generated. */
- @Parameter(names = {"--testwise-coverage", "-t"}, required = false, arity = 0, description = "Whether testwise " +
- "coverage or jacoco coverage should be generated.")
- /* package */ boolean shouldGenerateTestwiseCoverage = false;
-
- /** After how many tests testwise coverage should be split into multiple reports. */
- @Parameter(names = {"--split-after", "-s"}, required = false, arity = 1, description = "After how many tests " +
- "testwise coverage should be split into multiple reports (Default is 5000).")
- private int splitAfter = 5000;
-
- /** @see #classDirectoriesOrZips */
- public List getClassDirectoriesOrZips() throws IOException {
- return ClasspathUtils
- .resolveClasspathTextFiles("class-dir", new FilePatternResolver(new CommandLineLogger()),
- classDirectoriesOrZips);
- }
-
- /** @see #locationIncludeFilters */
- public List getLocationIncludeFilters() {
- return locationIncludeFilters;
- }
-
- /** @see #locationExcludeFilters */
- public List getLocationExcludeFilters() {
- return locationExcludeFilters;
- }
-
- /** @see #inputFiles */
- public List getInputFiles() {
- return inputFiles.stream().map(File::new).collect(Collectors.toList());
- }
-
- /** @see #outputFile */
- public File getOutputFile() {
- return new File(outputFile);
- }
-
- /** @see #splitAfter */
- public int getSplitAfter() {
- return splitAfter;
- }
-
- /** @see #duplicateClassFileBehavior */
- public EDuplicateClassFileBehavior getDuplicateClassFileBehavior() {
- return duplicateClassFileBehavior;
- }
-
- /** Makes sure the arguments are valid. */
- @Override
- public Validator validate() {
- Validator validator = new Validator();
-
- List classDirectoriesOrZips = new ArrayList<>();
- validator.ensure(() -> classDirectoriesOrZips.addAll(getClassDirectoriesOrZips()));
- validator.isFalse(classDirectoriesOrZips.isEmpty(),
- "You must specify at least one directory or zip that contains class files");
- for (File path : classDirectoriesOrZips) {
- validator.isTrue(path.exists(), "Path '" + path + "' does not exist");
- validator.isTrue(path.canRead(), "Path '" + path + "' is not readable");
- }
-
- for (File inputFile : getInputFiles()) {
- validator.isTrue(inputFile.exists() && inputFile.canRead(),
- "Cannot read the input file " + inputFile);
- }
-
- validator.ensure(() -> {
- Assertions.isFalse(StringUtils.isEmpty(outputFile), "You must specify an output file");
- File outputDir = getOutputFile().getAbsoluteFile().getParentFile();
- FileSystemUtils.ensureDirectoryExists(outputDir);
- Assertions.isTrue(outputDir.canWrite(), "Path '" + outputDir + "' is not writable");
- });
-
- return validator;
- }
-
- /** {@inheritDoc} */
- @Override
- public void run() throws Exception {
- Converter converter = new Converter(this);
- if (this.shouldGenerateTestwiseCoverage) {
- converter.runTestwiseCoverageReportGeneration();
- } else {
- converter.runJaCoCoReportGeneration();
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/convert/Converter.java b/agent/src/main/java/com/teamscale/jacoco/agent/convert/Converter.java
deleted file mode 100644
index e46cc5852..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/convert/Converter.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.teamscale.jacoco.agent.convert;
-
-import com.teamscale.client.TestDetails;
-import com.teamscale.jacoco.agent.logging.LoggingUtils;
-import com.teamscale.jacoco.agent.options.AgentOptionParseException;
-import com.teamscale.jacoco.agent.util.Benchmark;
-import com.teamscale.report.ReportUtils;
-import com.teamscale.report.jacoco.EmptyReportException;
-import com.teamscale.report.jacoco.JaCoCoXmlReportGenerator;
-import com.teamscale.report.testwise.ETestArtifactFormat;
-import com.teamscale.report.testwise.TestwiseCoverageReportWriter;
-import com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator;
-import com.teamscale.report.testwise.model.TestExecution;
-import com.teamscale.report.testwise.model.factory.TestInfoFactory;
-import com.teamscale.report.util.ClasspathWildcardIncludeFilter;
-import com.teamscale.report.util.CommandLineLogger;
-import com.teamscale.report.util.ILogger;
-import org.slf4j.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Paths;
-import java.util.List;
-
-import static com.teamscale.jacoco.agent.logging.LoggingUtils.wrap;
-
-/** Converts one .exec binary coverage file to XML. */
-public class Converter {
-
- /** The command line arguments. */
- private ConvertCommand arguments;
-
- /** Constructor. */
- public Converter(ConvertCommand arguments) {
- this.arguments = arguments;
- }
-
- /** Converts one .exec binary coverage file to XML. */
- public void runJaCoCoReportGeneration() throws IOException {
- List jacocoExecutionDataList = ReportUtils
- .listFiles(ETestArtifactFormat.JACOCO, arguments.getInputFiles());
-
- Logger logger = LoggingUtils.getLogger(this);
- JaCoCoXmlReportGenerator generator = new JaCoCoXmlReportGenerator(arguments.getClassDirectoriesOrZips(),
- getWildcardIncludeExcludeFilter(), arguments.getDuplicateClassFileBehavior(),
- arguments.shouldIgnoreUncoveredClasses,
- wrap(logger));
-
- try (Benchmark benchmark = new Benchmark("Generating the XML report")) {
- generator.convertExecFilesToReport(jacocoExecutionDataList, Paths.get(arguments.outputFile).toFile());
- } catch (EmptyReportException e) {
- logger.warn("Converted report was empty.", e);
- }
- }
-
- /** Converts one .exec binary coverage file, test details and test execution files to JSON testwise coverage. */
- public void runTestwiseCoverageReportGeneration() throws IOException, AgentOptionParseException {
- List testDetails = ReportUtils.readObjects(ETestArtifactFormat.TEST_LIST,
- TestDetails[].class, arguments.getInputFiles());
- List testExecutions = ReportUtils.readObjects(ETestArtifactFormat.TEST_EXECUTION,
- TestExecution[].class, arguments.getInputFiles());
-
- List jacocoExecutionDataList = ReportUtils
- .listFiles(ETestArtifactFormat.JACOCO, arguments.getInputFiles());
- ILogger logger = new CommandLineLogger();
-
- JaCoCoTestwiseReportGenerator generator = new JaCoCoTestwiseReportGenerator(
- arguments.getClassDirectoriesOrZips(),
- getWildcardIncludeExcludeFilter(),
- arguments.getDuplicateClassFileBehavior(),
- logger
- );
-
- TestInfoFactory testInfoFactory = new TestInfoFactory(testDetails, testExecutions);
-
- try (Benchmark benchmark = new Benchmark("Generating the testwise coverage report")) {
- logger.info(
- "Writing report with " + testDetails.size() + " Details/" + testExecutions.size() + " Results");
-
- try (TestwiseCoverageReportWriter coverageWriter = new TestwiseCoverageReportWriter(testInfoFactory,
- arguments.getOutputFile(), arguments.getSplitAfter(), null)) {
- for (File executionDataFile : jacocoExecutionDataList) {
- generator.convertAndConsume(executionDataFile, coverageWriter);
- }
- }
- }
- }
-
- private ClasspathWildcardIncludeFilter getWildcardIncludeExcludeFilter() {
- return new ClasspathWildcardIncludeFilter(
- String.join(":", arguments.getLocationIncludeFilters()),
- String.join(":", arguments.getLocationExcludeFilters()));
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.java
deleted file mode 100644
index ce83d7422..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.teamscale.jacoco.agent.logging;
-
-import java.nio.file.Path;
-
-/** Defines a property that contains the path to which log files should be written. */
-public class DebugLogDirectoryPropertyDefiner extends LogDirectoryPropertyDefiner {
-
- /** File path for debug logging. */
- /* package */ static Path filePath = null;
-
- @Override
- public String getPropertyValue() {
- if (filePath == null) {
- return super.getPropertyValue();
- }
- return filePath.resolve("logs").toAbsolutePath().toString();
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.java
deleted file mode 100644
index a10c57221..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.teamscale.jacoco.agent.logging;
-
-import ch.qos.logback.core.PropertyDefinerBase;
-import com.teamscale.jacoco.agent.util.AgentUtils;
-
-import java.nio.file.Path;
-
-/** Defines a property that contains the default path to which log files should be written. */
-public class LogDirectoryPropertyDefiner extends PropertyDefinerBase {
- @Override
- public String getPropertyValue() {
- Path tempDirectory = AgentUtils.getMainTempDirectory();
- return tempDirectory.resolve("logs").toAbsolutePath().toString();
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java
deleted file mode 100644
index 4a1906388..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.java
+++ /dev/null
@@ -1,207 +0,0 @@
-package com.teamscale.jacoco.agent.logging;
-
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.AppenderBase;
-import ch.qos.logback.core.status.ErrorStatus;
-import com.teamscale.client.ITeamscaleService;
-import com.teamscale.client.ProfilerLogEntry;
-import com.teamscale.client.TeamscaleClient;
-import com.teamscale.jacoco.agent.options.AgentOptions;
-import org.jetbrains.annotations.Nullable;
-import retrofit2.Call;
-
-import java.net.ConnectException;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.IdentityHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import static com.teamscale.jacoco.agent.logging.LoggingUtils.getStackTraceFromEvent;
-
-/**
- * Custom log appender that sends logs to Teamscale; it buffers log that were not sent due to connection issues and
- * sends them later.
- */
-public class LogToTeamscaleAppender extends AppenderBase {
-
- /** Flush the logs after N elements are in the queue */
- private static final int BATCH_SIZE = 50;
-
- /** Flush the logs in the given time interval */
- private static final Duration FLUSH_INTERVAL = Duration.ofSeconds(3);
-
- /** The unique ID of the profiler */
- private String profilerId;
-
- /** The service client for sending logs to Teamscale */
- private static ITeamscaleService teamscaleClient;
-
- /**
- * Buffer for unsent logs. We use a set here to allow for removing entries fast after sending them to Teamscale was
- * successful.
- */
- private final LinkedHashSet logBuffer = new LinkedHashSet<>();
-
- /** Scheduler for sending logs after the configured time interval */
- private final ScheduledExecutorService scheduler;
-
- /** Active log flushing threads */
- private final Set> activeLogFlushes = Collections.newSetFromMap(new IdentityHashMap<>());
-
- /** Is there a flush going on right now? */
- private final AtomicBoolean isFlusing = new AtomicBoolean(false);
-
- public LogToTeamscaleAppender() {
- this.scheduler = Executors.newScheduledThreadPool(1, r -> {
- // Make the thread a daemon so that it does not prevent the JVM from terminating.
- Thread t = Executors.defaultThreadFactory().newThread(r);
- t.setDaemon(true);
- return t;
- });
- }
-
- @Override
- public void start() {
- super.start();
- scheduler.scheduleAtFixedRate(() -> {
- synchronized (activeLogFlushes) {
- activeLogFlushes.removeIf(CompletableFuture::isDone);
- if (this.activeLogFlushes.isEmpty()) {
- flush();
- }
- }
- }, FLUSH_INTERVAL.toMillis(), FLUSH_INTERVAL.toMillis(), TimeUnit.MILLISECONDS);
- }
-
- @Override
- protected void append(ILoggingEvent eventObject) {
- synchronized (logBuffer) {
- logBuffer.add(formatLog(eventObject));
- if (logBuffer.size() >= BATCH_SIZE) {
- flush();
- }
- }
- }
-
- private ProfilerLogEntry formatLog(ILoggingEvent eventObject) {
- String trace = getStackTraceFromEvent(eventObject);
- long timestamp = eventObject.getTimeStamp();
- String message = eventObject.getFormattedMessage();
- String severity = eventObject.getLevel().toString();
- return new ProfilerLogEntry(timestamp, message, trace, severity);
- }
-
- private void flush() {
- sendLogs();
- }
-
- /** Send logs in a separate thread */
- private void sendLogs() {
- synchronized (activeLogFlushes) {
- activeLogFlushes.add(CompletableFuture.runAsync(() -> {
- if (isFlusing.compareAndSet(false, true)) {
- try {
- if (teamscaleClient == null) {
- // There might be no connection configured.
- return;
- }
-
- List logsToSend;
- synchronized (logBuffer) {
- logsToSend = new ArrayList<>(logBuffer);
- }
-
- Call call = teamscaleClient.postProfilerLog(profilerId, logsToSend);
- retrofit2.Response response = call.execute();
- if (!response.isSuccessful()) {
- throw new IllegalStateException("Failed to send log: HTTP error code : " + response.code());
- }
-
- synchronized (logBuffer) {
- // Removing the logs that have been sent after the fact.
- // This handles problems with lost network connections.
- logsToSend.forEach(logBuffer::remove);
- }
- } catch (Exception e) {
- // We do not report on exceptions here.
- if (!(e instanceof ConnectException)) {
- addStatus(new ErrorStatus("Sending logs to Teamscale failed: " + e.getMessage(), this, e));
- }
- } finally {
- isFlusing.set(false);
- }
- }
- }).whenComplete((result, throwable) -> {
- synchronized (activeLogFlushes) {
- activeLogFlushes.removeIf(CompletableFuture::isDone);
- }
- }));
- }
- }
-
- @Override
- public void stop() {
- // Already flush here once to make sure that we do not miss too much.
- flush();
-
- scheduler.shutdown();
- try {
- if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
- scheduler.shutdownNow();
- }
- } catch (InterruptedException e) {
- scheduler.shutdownNow();
- }
-
- // A final flush after the scheduler has been shut down.
- flush();
-
- // Block until all flushes are done
- CompletableFuture.allOf(activeLogFlushes.toArray(new CompletableFuture[0])).join();
-
- super.stop();
- }
-
- public void setTeamscaleClient(ITeamscaleService teamscaleClient) {
- this.teamscaleClient = teamscaleClient;
- }
-
- public void setProfilerId(String profilerId) {
- this.profilerId = profilerId;
- }
-
- /**
- * Add the {@link com.teamscale.jacoco.agent.logging.LogToTeamscaleAppender} to the logging configuration and
- * enable/start it.
- */
- public static boolean addTeamscaleAppenderTo(LoggerContext context, AgentOptions agentOptions) {
- @Nullable TeamscaleClient client = agentOptions.createTeamscaleClient(
- false);
- if (client == null || agentOptions.configurationViaTeamscale == null) {
- return false;
- }
-
- ITeamscaleService serviceClient = client.getService();
- LogToTeamscaleAppender logToTeamscaleAppender = new LogToTeamscaleAppender();
- logToTeamscaleAppender.setContext(context);
- logToTeamscaleAppender.setProfilerId(agentOptions.configurationViaTeamscale.getProfilerId());
- logToTeamscaleAppender.setTeamscaleClient(serviceClient);
- logToTeamscaleAppender.start();
-
- Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME);
- rootLogger.addAppender(logToTeamscaleAppender);
-
- return true;
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java b/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java
deleted file mode 100644
index d836d8e6f..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/logging/LoggingUtils.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2018 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.logging;
-
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.joran.JoranConfigurator;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.classic.spi.IThrowableProxy;
-import ch.qos.logback.classic.spi.ThrowableProxy;
-import ch.qos.logback.classic.spi.ThrowableProxyUtil;
-import ch.qos.logback.core.joran.spi.JoranException;
-import ch.qos.logback.core.util.StatusPrinter;
-import com.teamscale.jacoco.agent.Agent;
-import com.teamscale.jacoco.agent.util.NullOutputStream;
-import com.teamscale.report.util.ILogger;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintStream;
-import java.nio.file.Path;
-
-/**
- * Helps initialize the logging framework properly.
- */
-public class LoggingUtils {
-
- /** Returns a logger for the given object's class. */
- public static Logger getLogger(Object object) {
- return LoggerFactory.getLogger(object.getClass());
- }
-
- /** Returns a logger for the given class. */
- public static Logger getLogger(Class> object) {
- return LoggerFactory.getLogger(object);
- }
-
- /** Class to use with try-with-resources to close the logging framework's resources. */
- public static class LoggingResources implements AutoCloseable {
-
- @Override
- public void close() {
- getLoggerContext().stop();
- }
- }
-
- /** Initializes the logging to the default configured in the Jar. */
- public static LoggingResources initializeDefaultLogging() {
- InputStream stream = Agent.class.getResourceAsStream("logback-default.xml");
- reconfigureLoggerContext(stream);
- return new LoggingResources();
- }
-
- /**
- * Returns the logger context.
- */
- public static LoggerContext getLoggerContext() {
- return (LoggerContext) LoggerFactory.getILoggerFactory();
- }
-
- /**
- * Extracts the stack trace from an ILoggingEvent using ThrowableProxyUtil.
- *
- * @param event the logging event containing the exception
- * @return the stack trace as a String, or null if no exception is associated
- */
- public static String getStackTraceFromEvent(ILoggingEvent event) {
- IThrowableProxy throwableProxy = event.getThrowableProxy();
-
- if (throwableProxy != null) {
- // Use ThrowableProxyUtil to convert the IThrowableProxy to a String
- return ThrowableProxyUtil.asString(throwableProxy);
- }
-
- return null;
- }
-
- /**
- * Converts a Throwable to its stack trace as a String.
- *
- * @param throwable the throwable to convert
- * @return the stack trace as a String
- */
- public static String getStackTraceAsString(Throwable throwable) {
- if (throwable == null) {
- return null;
- }
- return ThrowableProxyUtil.asString(new ThrowableProxy(throwable));
- }
-
- /**
- * Reconfigures the logger context to use the configuration XML from the given input stream. Cf. https://logback.qos.ch/manual/configuration.html
- */
- private static void reconfigureLoggerContext(InputStream stream) {
- StatusPrinter.setPrintStream(new PrintStream(new NullOutputStream()));
- LoggerContext loggerContext = getLoggerContext();
- try {
- JoranConfigurator configurator = new JoranConfigurator();
- configurator.setContext(loggerContext);
- loggerContext.reset();
- configurator.doConfigure(stream);
- } catch (JoranException je) {
- // StatusPrinter will handle this
- }
- StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);
- }
-
- /**
- * Initializes the logging from the given file. If that is null, uses {@link
- * #initializeDefaultLogging()} instead.
- */
- public static LoggingResources initializeLogging(Path loggingConfigFile) throws IOException {
- if (loggingConfigFile == null) {
- return initializeDefaultLogging();
- }
-
- reconfigureLoggerContext(new FileInputStream(loggingConfigFile.toFile()));
- return new LoggingResources();
- }
-
- /** Initializes debug logging. */
- public static LoggingResources initializeDebugLogging(Path logDirectory) {
- if (logDirectory != null) {
- DebugLogDirectoryPropertyDefiner.filePath = logDirectory;
- }
- InputStream stream = Agent.class.getResourceAsStream("logback-default-debugging.xml");
- reconfigureLoggerContext(stream);
- return new LoggingResources();
- }
-
- /** Wraps the given slf4j logger into an {@link ILogger}. */
- public static ILogger wrap(Logger logger) {
- return new ILogger() {
- @Override
- public void debug(String message) {
- logger.debug(message);
- }
-
- @Override
- public void info(String message) {
- logger.info(message);
- }
-
- @Override
- public void warn(String message) {
- logger.warn(message);
- }
-
- @Override
- public void warn(String message, Throwable throwable) {
- logger.warn(message, throwable);
- }
-
- @Override
- public void error(Throwable throwable) {
- logger.error(throwable.getMessage(), throwable);
- }
-
- @Override
- public void error(String message, Throwable throwable) {
- logger.error(message, throwable);
- }
- };
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java
deleted file mode 100644
index e055ee8c1..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.teamscale.jacoco.agent.util;
-
-import com.teamscale.client.FileSystemUtils;
-import com.teamscale.client.TeamscaleServiceGenerator;
-import com.teamscale.jacoco.agent.PreMain;
-import com.teamscale.jacoco.agent.configuration.ProcessInformationRetriever;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ResourceBundle;
-
-/** General utilities for working with the agent. */
-public class AgentUtils {
-
- /** Version of this program. */
- public static final String VERSION;
-
- /** User-Agent header value for HTTP requests. */
- public static final String USER_AGENT;
-
- private static Path mainTempDirectory = null;
-
- static {
- ResourceBundle bundle = ResourceBundle.getBundle("com.teamscale.jacoco.agent.app");
- VERSION = bundle.getString("version");
- USER_AGENT = TeamscaleServiceGenerator.buildUserAgent("Teamscale Java Profiler", VERSION);
- }
-
- /**
- * Returns the main temporary directory where all agent temp files should be placed.
- */
- public static Path getMainTempDirectory() {
- if (mainTempDirectory == null) {
- try {
- // We add a trailing hyphen here to visually separate the PID from the random number that Java appends
- // to the name to make it unique
- mainTempDirectory = Files.createTempDirectory("teamscale-java-profiler-" +
- FileSystemUtils.toSafeFilename(ProcessInformationRetriever.getPID()) + "-");
- } catch (IOException e) {
- throw new RuntimeException("Failed to create temporary directory for agent files", e);
- }
- }
- return mainTempDirectory;
- }
-
- /** Returns the directory that contains the agent installation. */
- public static Path getAgentDirectory() {
- try {
- URI jarFileUri = PreMain.class.getProtectionDomain().getCodeSource().getLocation().toURI();
- // we assume that the dist zip is extracted and the agent jar not moved
- Path jarDirectory = Paths.get(jarFileUri).getParent();
- Path installDirectory = jarDirectory.getParent();
- if (installDirectory == null) {
- // happens when the jar file is stored in the root directory
- return jarDirectory;
- }
- return installDirectory;
- } catch (URISyntaxException e) {
- throw new RuntimeException("Failed to obtain agent directory. This is a bug, please report it.", e);
- }
- }
-
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/Assertions.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/Assertions.java
deleted file mode 100644
index b789c5f6a..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/Assertions.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.teamscale.jacoco.agent.util;
-
-import org.jetbrains.annotations.Contract;
-
-/**
- * Simple methods to implement assertions.
- */
-public class Assertions {
-
- /**
- * Checks if a condition is true.
- *
- * @param condition condition to check
- * @param message exception message
- * @throws AssertionError if the condition is false
- */
- @Contract(value = "false, _ -> fail", pure = true)
- public static void isTrue(boolean condition, String message) throws AssertionError {
- throwAssertionErrorIfTestFails(condition, message);
- }
-
- /**
- * Checks if a condition is false.
- *
- * @param condition condition to check
- * @param message exception message
- * @throws AssertionError if the condition is true
- */
- @Contract(value = "true, _ -> fail", pure = true)
- public static void isFalse(boolean condition, String message) throws AssertionError {
- throwAssertionErrorIfTestFails(!condition, message);
- }
-
- /**
- * Throws an {@link AssertionError} if the test fails.
- *
- * @param test test which should be true
- * @param message exception message
- * @throws AssertionError if the test fails
- */
- private static void throwAssertionErrorIfTestFails(boolean test, String message) {
- if (!test) {
- throw new AssertionError(message);
- }
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/DaemonThreadFactory.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/DaemonThreadFactory.java
deleted file mode 100644
index 1ec3461ec..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/DaemonThreadFactory.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.teamscale.jacoco.agent.util;
-
-import java.util.concurrent.ThreadFactory;
-
-/**
- * {@link ThreadFactory} that only produces deamon threads (threads that don't prevent JVM shutdown) with a fixed name.
- */
-public class DaemonThreadFactory implements ThreadFactory {
-
- private final String threadName;
-
- public DaemonThreadFactory(Class> owningClass, String threadName) {
- this.threadName = "Teamscale Java Profiler " + owningClass.getSimpleName() + " " + threadName;
- }
-
- @Override
- public Thread newThread(Runnable runnable) {
- Thread thread = new Thread(runnable, threadName);
- thread.setDaemon(true);
- return thread;
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/NullOutputStream.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/NullOutputStream.java
deleted file mode 100644
index 856e316a1..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/NullOutputStream.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.teamscale.jacoco.agent.util;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/** NOP output stream implementation. */
-public class NullOutputStream extends OutputStream {
-
- public NullOutputStream() {
- // do nothing
- }
-
- @Override
- public void write(final byte @NotNull [] b, final int off, final int len) {
- // to /dev/null
- }
-
- @Override
- public void write(final int b) {
- // to /dev/null
- }
-
- @Override
- public void write(final byte @NotNull [] b) throws IOException {
- // to /dev/null
- }
-}
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/Timer.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/Timer.java
deleted file mode 100644
index a6787e085..000000000
--- a/agent/src/main/java/com/teamscale/jacoco/agent/util/Timer.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*-------------------------------------------------------------------------+
-| |
-| Copyright (c) 2009-2018 CQSE GmbH |
-| |
-+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.util;
-
-import java.time.Duration;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Triggers a callback in a regular interval. Note that the spawned threads are
- * Daemon threads, i.e. they will not prevent the JVM from shutting down.
- *
- * The timer will abort if the given {@link #runnable} ever throws an exception.
- */
-public class Timer {
-
- /** Runs the job on a background daemon thread. */
- private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, runnable -> {
- Thread thread = Executors.defaultThreadFactory().newThread(runnable);
- thread.setDaemon(true);
- return thread;
- });
-
- /** The currently running job or null. */
- private ScheduledFuture> job = null;
-
- /** The job to execute periodically. */
- private final Runnable runnable;
-
- /** Duration between two job executions. */
- private final Duration duration;
-
- /** Constructor. */
- public Timer(Runnable runnable, Duration duration) {
- this.runnable = runnable;
- this.duration = duration;
- }
-
- /** Starts the regular job. */
- public synchronized void start() {
- if (job != null) {
- return;
- }
-
- job = executor.scheduleAtFixedRate(runnable, duration.toMinutes(), duration.toMinutes(), TimeUnit.MINUTES);
- }
-
- /** Stops the regular job, possibly aborting it. */
- public synchronized void stop() {
- job.cancel(false);
- job = null;
- }
-
-}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt
new file mode 100644
index 000000000..010e88409
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Agent.kt
@@ -0,0 +1,170 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.client.FileSystemUtils
+import com.teamscale.client.StringUtils
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.options.AgentOptions
+import com.teamscale.jacoco.agent.upload.IUploadRetry
+import com.teamscale.jacoco.agent.upload.IUploader
+import com.teamscale.jacoco.agent.upload.teamscale.TeamscaleUploader
+import com.teamscale.jacoco.agent.util.AgentUtils
+import com.teamscale.report.jacoco.CoverageFile
+import com.teamscale.report.jacoco.EmptyReportException
+import com.teamscale.report.jacoco.JaCoCoXmlReportGenerator
+import com.teamscale.report.jacoco.dump.Dump
+import org.glassfish.jersey.server.ResourceConfig
+import org.glassfish.jersey.server.ServerProperties
+import java.io.File
+import java.io.IOException
+import java.lang.instrument.Instrumentation
+import java.nio.file.Files
+import java.util.Timer
+import kotlin.concurrent.fixedRateTimer
+import kotlin.io.path.deleteIfExists
+import kotlin.io.path.listDirectoryEntries
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+/**
+ * A wrapper around the JaCoCo Java agent that automatically triggers a dump and XML conversion based on a time
+ * interval.
+ */
+class Agent(options: AgentOptions, instrumentation: Instrumentation?) : AgentBase(options) {
+ /** Converts binary data to XML. */
+ private val generator: JaCoCoXmlReportGenerator
+
+ /** Regular dump task. */
+ private var timer: Timer? = null
+
+ /** Stores the XML files. */
+ private val uploader = options.createUploader(instrumentation)
+
+ /** Constructor. */
+ init {
+ logger.info("Upload method: {}", uploader.describe())
+ retryUnsuccessfulUploads(options, uploader)
+ generator = JaCoCoXmlReportGenerator(
+ options.getClassDirectoriesOrZips(),
+ options.locationIncludeFilter,
+ options.getDuplicateClassFileBehavior(),
+ options.shouldIgnoreUncoveredClasses(),
+ LoggingUtils.wrap(logger)
+ )
+
+ if (options.shouldDumpInIntervals()) {
+ val period = options.dumpIntervalInMinutes.toDuration(DurationUnit.MINUTES).inWholeMilliseconds
+ timer = fixedRateTimer("Teamscale-Java-Profiler", true, period, period) {
+ dumpReport()
+ }
+ logger.info("Dumping every ${options.dumpIntervalInMinutes} minutes.")
+ }
+ options.teamscaleServerOptions.partition?.let { partition ->
+ controller.sessionId = partition
+ }
+ }
+
+ /**
+ * If we have coverage that was leftover because of previously unsuccessful coverage uploads, we retry to upload
+ * them again with the same configuration as in the previous try.
+ */
+ private fun retryUnsuccessfulUploads(options: AgentOptions, uploader: IUploader) {
+ var outputPath = options.outputDirectory
+ if (outputPath == null) {
+ // Default fallback
+ outputPath = AgentUtils.agentDirectory.resolve("coverage")
+ }
+
+ val parentPath = outputPath.parent
+ if (parentPath == null) {
+ logger.error("The output path '{}' does not have a parent path. Canceling upload retry.", outputPath.toAbsolutePath())
+ return
+ }
+
+ parentPath.toFile().walk()
+ .filter { it.name.endsWith(TeamscaleUploader.RETRY_UPLOAD_FILE_SUFFIX) }
+ .forEach { file ->
+ reuploadCoverageFromPropertiesFile(file, uploader)
+ }
+ }
+
+ private fun reuploadCoverageFromPropertiesFile(file: File, uploader: IUploader) {
+ logger.info("Retrying previously unsuccessful coverage upload for file {}.", file)
+ try {
+ val properties = FileSystemUtils.readProperties(file)
+ val coverageFile = CoverageFile(
+ File(StringUtils.stripSuffix(file.absolutePath, TeamscaleUploader.RETRY_UPLOAD_FILE_SUFFIX))
+ )
+
+ if (uploader is IUploadRetry) {
+ uploader.reupload(coverageFile, properties)
+ } else {
+ logger.info("Reupload not implemented for uploader {}", uploader.describe())
+ }
+ Files.deleteIfExists(file.toPath())
+ } catch (e: IOException) {
+ logger.error("Reuploading coverage failed. $e")
+ }
+ }
+
+ override fun initResourceConfig(): ResourceConfig? {
+ val resourceConfig = ResourceConfig()
+ resourceConfig.property(ServerProperties.WADL_FEATURE_DISABLE, true.toString())
+ AgentResource.setAgent(this)
+ return resourceConfig.register(AgentResource::class.java).register(GenericExceptionMapper::class.java)
+ }
+
+ override fun prepareShutdown() {
+ timer?.cancel()
+ if (options.shouldDumpOnExit()) dumpReport()
+
+ val dir = options.outputDirectory
+ try {
+ if (dir.listDirectoryEntries().isEmpty()) dir.deleteIfExists()
+ } catch (e: IOException) {
+ logger.info(
+ ("Could not delete empty output directory {}. "
+ + "This directory was created inside the configured output directory to be able to "
+ + "distinguish between different runs of the profiled JVM. You may delete it manually."),
+ dir, e
+ )
+ }
+ }
+
+ /**
+ * Dumps the current execution data, converts it, writes it to the output directory defined in [.options] and
+ * uploads it if an uploader is configured. Logs any errors, never throws an exception.
+ */
+ override fun dumpReport() {
+ logger.debug("Starting dump")
+
+ try {
+ dumpReportUnsafe()
+ } catch (t: Throwable) {
+ // we want to catch anything in order to avoid crashing the whole system under
+ // test
+ logger.error("Dump job failed with an exception", t)
+ }
+ }
+
+ private fun dumpReportUnsafe() {
+ val dump: Dump
+ try {
+ dump = controller.dumpAndReset()
+ } catch (e: JacocoRuntimeController.DumpException) {
+ logger.error("Dumping failed, retrying later", e)
+ return
+ }
+
+ try {
+ benchmark("Generating the XML report") {
+ val outputFile = options.createNewFileInOutputDirectory("jacoco", "xml")
+ val coverageFile = generator.convertSingleDumpToReport(dump, outputFile)
+ uploader.upload(coverageFile)
+ }
+ } catch (e: IOException) {
+ logger.error("Converting binary dump to XML failed", e)
+ } catch (e: EmptyReportException) {
+ logger.error("No coverage was collected. ${e.message}", e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentBase.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentBase.kt
new file mode 100644
index 000000000..bfed53514
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentBase.kt
@@ -0,0 +1,150 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.options.AgentOptions
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.server.ServerConnector
+import org.eclipse.jetty.servlet.ServletContextHandler
+import org.eclipse.jetty.servlet.ServletHolder
+import org.eclipse.jetty.util.thread.QueuedThreadPool
+import org.glassfish.jersey.server.ResourceConfig
+import org.glassfish.jersey.servlet.ServletContainer
+import org.jacoco.agent.rt.RT
+import org.slf4j.Logger
+import java.lang.management.ManagementFactory
+
+/**
+ * Base class for agent implementations. Handles logger shutdown, store creation and instantiation of the
+ * [JacocoRuntimeController].
+ *
+ *
+ * Subclasses must handle dumping onto disk and uploading via the configured uploader.
+ */
+abstract class AgentBase(
+ /** The agent options. */
+ @JvmField var options: AgentOptions
+) {
+ /** The logger. */
+ val logger: Logger = LoggingUtils.getLogger(this)
+
+ /** Controls the JaCoCo runtime. */
+ @JvmField
+ val controller: JacocoRuntimeController
+
+ private lateinit var server: Server
+
+ /**
+ * Lazily generated string representation of the command line arguments to print to the log.
+ */
+ private val optionsObjectToLog by lazy {
+ object {
+ override fun toString() =
+ if (options.shouldObfuscateSecurityRelatedOutputs()) {
+ options.getObfuscatedOptionsString()
+ } else {
+ options.getOriginalOptionsString()
+ }
+ }
+ }
+
+ init {
+ try {
+ controller = JacocoRuntimeController(RT.getAgent())
+ } catch (e: IllegalStateException) {
+ throw IllegalStateException("Teamscale Java Profiler not started or there is a conflict with another agent on the classpath.", e)
+ }
+ logger.info(
+ "Starting Teamscale Java Profiler for process {} with options: {}",
+ ManagementFactory.getRuntimeMXBean().name, optionsObjectToLog
+ )
+ options.getHttpServerPort()?.let { port ->
+ try {
+ initServer()
+ } catch (e: Exception) {
+ logger.error("Could not start http server on port $port. Please check if the port is blocked.")
+ throw IllegalStateException("Control server not started.", e)
+ }
+ }
+ }
+
+ /**
+ * Starts the http server, which waits for information about started and finished tests.
+ */
+ @Throws(Exception::class)
+ private fun initServer() {
+ logger.info("Listening for test events on port {}.", options.getHttpServerPort())
+
+ // Jersey Implementation
+ val handler = buildUsingResourceConfig()
+ val threadPool = QueuedThreadPool()
+ threadPool.maxThreads = 10
+ threadPool.isDaemon = true
+
+ // Create a server instance and set the thread pool
+ server = Server(threadPool)
+ // Create a server connector, set the port and add it to the server
+ val connector = ServerConnector(server)
+ connector.port = options.getHttpServerPort()
+ server.addConnector(connector)
+ server.handler = handler
+ server.start()
+ }
+
+ private fun buildUsingResourceConfig(): ServletContextHandler {
+ val handler = ServletContextHandler(ServletContextHandler.NO_SESSIONS)
+ handler.contextPath = "/"
+
+ val resourceConfig = initResourceConfig()
+ handler.addServlet(ServletHolder(ServletContainer(resourceConfig)), "/*")
+ return handler
+ }
+
+ /**
+ * Initializes the [ResourceConfig] needed for the Jetty + Jersey Server
+ */
+ protected abstract fun initResourceConfig(): ResourceConfig?
+
+ /**
+ * Registers a shutdown hook that stops the timer and dumps coverage a final time.
+ */
+ fun registerShutdownHook() {
+ Runtime.getRuntime().addShutdownHook(Thread {
+ try {
+ logger.info("Teamscale Java Profiler is shutting down...")
+ stopServer()
+ prepareShutdown()
+ logger.info("Teamscale Java Profiler successfully shut down.")
+ } catch (e: Exception) {
+ logger.error("Exception during profiler shutdown.", e)
+ } finally {
+ // Try to flush logging resources also in case of an exception during shutdown
+ PreMain.closeLoggingResources()
+ }
+ })
+ }
+
+ /** Stop the http server if it's running */
+ fun stopServer() {
+ options.getHttpServerPort()?.let {
+ try {
+ server.stop()
+ } catch (e: Exception) {
+ logger.error("Could not stop server so it is killed now.", e)
+ } finally {
+ server.destroy()
+ }
+ }
+ }
+
+ /** Called when the shutdown hook is triggered. */
+ protected open fun prepareShutdown() {
+ // Template method to be overridden by subclasses.
+ }
+
+ /**
+ * Dumps the current execution data, converts it, writes it to the output
+ * directory defined in [.options] and uploads it if an uploader is
+ * configured. Logs any errors, never throws an exception.
+ */
+ abstract fun dumpReport()
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentResource.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentResource.kt
new file mode 100644
index 000000000..94447f400
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/AgentResource.kt
@@ -0,0 +1,41 @@
+package com.teamscale.jacoco.agent
+
+import javax.ws.rs.POST
+import javax.ws.rs.Path
+import javax.ws.rs.core.Response
+
+/**
+ * The resource of the Jersey + Jetty http server holding all the endpoints specific for the [Agent].
+ */
+@Path("/")
+class AgentResource : ResourceBase() {
+ /** Handles dumping a XML coverage report for coverage collected until now. */
+ @POST
+ @Path("/dump")
+ fun handleDump(): Response? {
+ logger.debug("Dumping report triggered via HTTP request")
+ agent.dumpReport()
+ return Response.noContent().build()
+ }
+
+ /** Handles resetting of coverage. */
+ @POST
+ @Path("/reset")
+ fun handleReset(): Response? {
+ logger.debug("Resetting coverage triggered via HTTP request")
+ agent.controller.reset()
+ return Response.noContent().build()
+ }
+
+ companion object {
+ private lateinit var agent: Agent
+
+ /**
+ * Static setter to inject the [Agent] to the resource.
+ */
+ fun setAgent(agent: Agent) {
+ Companion.agent = agent
+ agentBase = agent
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/DelayedLogger.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/DelayedLogger.kt
new file mode 100644
index 000000000..d242ff79d
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/DelayedLogger.kt
@@ -0,0 +1,52 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.report.util.ILogger
+import org.slf4j.Logger
+import java.util.function.Consumer
+
+/**
+ * A logger that buffers logs in memory and writes them to the actual logger at a later point. This is needed when stuff
+ * needs to be logged before the actual logging framework is initialized.
+ */
+class DelayedLogger : ILogger {
+ /** List of log actions that will be executed once the logger is initialized. */
+ private val logActions = mutableListOf Unit>()
+
+ override fun debug(message: String) {
+ logActions.add { debug(message) }
+ }
+
+ override fun info(message: String) {
+ logActions.add { info(message) }
+ }
+
+ override fun warn(message: String) {
+ logActions.add { warn(message) }
+ }
+
+ override fun warn(message: String, throwable: Throwable?) {
+ logActions.add { warn(message, throwable) }
+ }
+
+ override fun error(throwable: Throwable) {
+ logActions.add { error(throwable.message, throwable) }
+ }
+
+ override fun error(message: String, throwable: Throwable?) {
+ logActions.add { error(message, throwable) }
+ }
+
+ /**
+ * Logs an error and also writes the message to [System.err] to ensure the message is even logged in case
+ * setting up the logger itself fails for some reason (see TS-23151).
+ */
+ fun errorAndStdErr(message: String?, throwable: Throwable?) {
+ System.err.println(message)
+ logActions.add { error(message, throwable) }
+ }
+
+ /** Writes the logs to the given slf4j logger. */
+ fun logTo(logger: Logger) {
+ logActions.forEach { action -> action(logger) }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/GenericExceptionMapper.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/GenericExceptionMapper.kt
new file mode 100644
index 000000000..c7f0f9909
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/GenericExceptionMapper.kt
@@ -0,0 +1,18 @@
+package com.teamscale.jacoco.agent
+
+import javax.ws.rs.core.MediaType
+import javax.ws.rs.core.Response
+import javax.ws.rs.ext.ExceptionMapper
+import javax.ws.rs.ext.Provider
+
+/**
+ * Generates a [javax.ws.rs.core.Response] for an exception.
+ */
+@Provider
+class GenericExceptionMapper : ExceptionMapper {
+ override fun toResponse(e: Throwable?): Response =
+ Response.status(Response.Status.INTERNAL_SERVER_ERROR).apply {
+ type(MediaType.TEXT_PLAIN_TYPE)
+ entity("Message: ${e?.message}")
+ }.build()
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/Helpers.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Helpers.kt
new file mode 100644
index 000000000..d3dc43a97
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Helpers.kt
@@ -0,0 +1,6 @@
+package com.teamscale.jacoco.agent
+
+import kotlin.time.measureTime
+
+fun benchmark(name: String, action: () -> Unit) =
+ measureTime { action() }.also { duration -> Main.logger.debug("$name took $duration") }
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt
new file mode 100644
index 000000000..0a76c0d3e
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/JacocoRuntimeController.kt
@@ -0,0 +1,118 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.report.jacoco.dump.Dump
+import org.jacoco.agent.rt.IAgent
+import org.jacoco.core.data.ExecutionData
+import org.jacoco.core.data.ExecutionDataReader
+import org.jacoco.core.data.ExecutionDataStore
+import org.jacoco.core.data.IExecutionDataVisitor
+import org.jacoco.core.data.ISessionInfoVisitor
+import org.jacoco.core.data.SessionInfo
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+
+/**
+ * Wrapper around JaCoCo's [RT] runtime interface.
+ *
+ *
+ * Can be used if the calling code is run in the same JVM as the agent is attached to.
+ */
+class JacocoRuntimeController
+/** Constructor. */(
+ /** JaCoCo's [RT] agent instance */
+ private val agent: IAgent
+) {
+ /** Indicates a failed dump. */
+ class DumpException(message: String?, cause: Throwable?) : Exception(message, cause)
+
+ /**
+ * Dumps execution data and resets it.
+ *
+ * @throws DumpException if dumping fails. This should never happen in real life. Dumping should simply be retried
+ * later if this ever happens.
+ */
+ @Throws(DumpException::class)
+ fun dumpAndReset(): Dump {
+ val binaryData = agent.getExecutionData(true)
+
+ try {
+ ByteArrayInputStream(binaryData).use { inputStream ->
+ ExecutionDataReader(inputStream).apply {
+ val store = ExecutionDataStore()
+ setExecutionDataVisitor { store.put(it) }
+ val sessionInfoVisitor = SessionInfoVisitor()
+ setSessionInfoVisitor(sessionInfoVisitor)
+ read()
+ return Dump(sessionInfoVisitor.sessionInfo, store)
+ }
+ }
+ } catch (e: IOException) {
+ throw DumpException("should never happen for the ByteArrayInputStream", e)
+ }
+ }
+
+ /**
+ * Dumps execution data to the given file and resets it afterwards.
+ */
+ @Throws(IOException::class)
+ fun dumpToFileAndReset(file: File) {
+ val binaryData = agent.getExecutionData(true)
+
+ FileOutputStream(file, true).use { outputStream ->
+ outputStream.write(binaryData)
+ }
+ }
+
+
+ /**
+ * Dumps execution data to a file and resets it.
+ *
+ * @throws DumpException if dumping fails. This should never happen in real life. Dumping should simply be retried
+ * later if this ever happens.
+ */
+ @Throws(DumpException::class)
+ fun dump() {
+ try {
+ agent.dump(true)
+ } catch (e: IOException) {
+ throw DumpException(e.message, e)
+ }
+ }
+
+ /** Resets already collected coverage. */
+ fun reset() {
+ agent.reset()
+ }
+
+ var sessionId: String?
+ /** Returns the current sessionId. */
+ get() = agent.sessionId
+ /**
+ * Sets the current sessionId of the agent that can be used to identify which coverage is recorded from now on.
+ */
+ set(sessionId) {
+ agent.setSessionId(sessionId)
+ }
+
+ /** Unsets the session ID so that coverage collected from now on is not attributed to the previous test. */
+ fun resetSessionId() {
+ agent.sessionId = ""
+ }
+
+ /**
+ * Receives and stores a [org.jacoco.core.data.SessionInfo]. Has a fallback dummy session in case nothing is received.
+ */
+ private class SessionInfoVisitor : ISessionInfoVisitor {
+ /** The received session info or a dummy. */
+ var sessionInfo: SessionInfo = SessionInfo(
+ "dummysession", System.currentTimeMillis(), System.currentTimeMillis()
+ )
+
+ /** {@inheritDoc} */
+ override fun visitSessionInfo(info: SessionInfo) {
+ this.sessionInfo = info
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/LenientCoverageTransformer.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/LenientCoverageTransformer.kt
new file mode 100644
index 000000000..dd9094e71
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/LenientCoverageTransformer.kt
@@ -0,0 +1,51 @@
+package com.teamscale.jacoco.agent
+
+import org.jacoco.agent.rt.internal_29a6edd.CoverageTransformer
+import org.jacoco.agent.rt.internal_29a6edd.IExceptionLogger
+import org.jacoco.agent.rt.internal_29a6edd.core.runtime.AgentOptions
+import org.jacoco.agent.rt.internal_29a6edd.core.runtime.IRuntime
+import org.slf4j.Logger
+import java.lang.instrument.IllegalClassFormatException
+import java.security.ProtectionDomain
+
+/**
+ * A class file transformer which delegates to the JaCoCo [org.jacoco.agent.rt.internal_29a6edd.CoverageTransformer] to do the actual instrumentation,
+ * but treats instrumentation errors e.g. due to unsupported class file versions more lenient by only logging them, but
+ * not bailing out completely. Those unsupported classes will not be instrumented and will therefore not be contained in
+ * the collected coverage report.
+ */
+class LenientCoverageTransformer(
+ runtime: IRuntime?,
+ options: AgentOptions,
+ private val logger: Logger
+) : CoverageTransformer(
+ runtime,
+ options,
+ // The coverage transformer only uses the logger to print an error when the instrumentation fails.
+ // We want to show our more specific error message instead, so we only log this for debugging at trace.
+ IExceptionLogger { logger.trace(it.message, it) }
+) {
+ override fun transform(
+ loader: ClassLoader?,
+ classname: String,
+ classBeingRedefined: Class<*>?,
+ protectionDomain: ProtectionDomain?,
+ classfileBuffer: ByteArray
+ ): ByteArray? {
+ try {
+ return super.transform(loader, classname, classBeingRedefined, protectionDomain, classfileBuffer)
+ } catch (e: IllegalClassFormatException) {
+ logger.error(
+ "Failed to instrument $classname. File will be skipped from instrumentation. " +
+ "No coverage will be collected for it. Exclude the file from the instrumentation or try " +
+ "updating the Teamscale Java Profiler if the file should actually be instrumented. (Cause: ${getRootCauseMessage(e)})"
+ )
+ return null
+ }
+ }
+
+ companion object {
+ private fun getRootCauseMessage(e: Throwable): String? =
+ e.cause?.let { getRootCauseMessage(it) } ?: e.message
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/Main.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Main.kt
new file mode 100644
index 000000000..e1efc8a12
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/Main.kt
@@ -0,0 +1,81 @@
+package com.teamscale.jacoco.agent
+
+import com.beust.jcommander.JCommander
+import com.beust.jcommander.Parameter
+import com.beust.jcommander.ParameterException
+import com.teamscale.client.StringUtils
+import com.teamscale.jacoco.agent.convert.ConvertCommand
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.util.AgentUtils
+import org.jacoco.core.JaCoCo
+import org.slf4j.Logger
+import kotlin.system.exitProcess
+
+/** Provides a command line interface for interacting with JaCoCo. */
+object Main {
+ /** The logger. */
+ val logger: Logger = LoggingUtils.getLogger(this)
+
+ /** The default arguments that will always be parsed. */
+ private val defaultArguments = DefaultArguments()
+
+ /** The arguments for the one-time conversion process. */
+ private val command = ConvertCommand()
+
+ /** Entry point. */
+ @Throws(Exception::class)
+ @JvmStatic
+ fun main(args: Array) {
+ parseCommandLineAndRun(args)
+ }
+
+ /**
+ * Parses the given command line arguments. Exits the program or throws an exception if the arguments are not valid.
+ * Then runs the specified command.
+ */
+ @Throws(Exception::class)
+ private fun parseCommandLineAndRun(args: Array) {
+ val builder = createJCommanderBuilder()
+ val jCommander = builder.build()
+
+ try {
+ jCommander.parse(*args)
+ } catch (e: ParameterException) {
+ handleInvalidCommandLine(jCommander, e.message)
+ }
+
+ if (defaultArguments.help) {
+ println("Teamscale Java Profiler ${AgentUtils.VERSION} compiled against JaCoCo ${JaCoCo.VERSION}")
+ jCommander.usage()
+ return
+ }
+
+ val validator = command.validate()
+ if (!validator.isValid) {
+ handleInvalidCommandLine(jCommander, StringUtils.LINE_FEED + validator.errorMessage)
+ }
+
+ logger.info("Starting Teamscale Java Profiler ${AgentUtils.VERSION} compiled against JaCoCo ${JaCoCo.VERSION}")
+ command.run()
+ }
+
+ /** Shows an informative error and help message. Then exits the program. */
+ private fun handleInvalidCommandLine(jCommander: JCommander, message: String?) {
+ System.err.println("Invalid command line: $message${StringUtils.LINE_FEED}")
+ jCommander.usage()
+ exitProcess(1)
+ }
+
+ /** Creates a builder for a [com.beust.jcommander.JCommander] object. */
+ private fun createJCommanderBuilder() =
+ JCommander.newBuilder().programName(Main::class.java.getName())
+ .addObject(defaultArguments)
+ .addObject(command)
+
+ /** Default arguments that may always be provided. */
+ private class DefaultArguments {
+ /** Shows the help message. */
+ @Parameter(names = ["--help"], help = true, description = "Shows all available command line arguments.")
+ val help = false
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt
new file mode 100644
index 000000000..403b756fa
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt
@@ -0,0 +1,307 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.client.FileSystemUtils
+import com.teamscale.client.HttpUtils
+import com.teamscale.client.StringUtils
+import com.teamscale.jacoco.agent.configuration.AgentOptionReceiveException
+import com.teamscale.jacoco.agent.logging.DebugLogDirectoryPropertyDefiner
+import com.teamscale.jacoco.agent.logging.LogDirectoryPropertyDefiner
+import com.teamscale.jacoco.agent.logging.LogToTeamscaleAppender
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.options.AgentOptionParseException
+import com.teamscale.jacoco.agent.options.AgentOptions
+import com.teamscale.jacoco.agent.options.AgentOptionsParser
+import com.teamscale.jacoco.agent.options.FilePatternResolver
+import com.teamscale.jacoco.agent.options.JacocoAgentOptionsBuilder
+import com.teamscale.jacoco.agent.options.TeamscalePropertiesUtils
+import com.teamscale.jacoco.agent.testimpact.TestwiseCoverageAgent
+import com.teamscale.jacoco.agent.upload.UploaderException
+import com.teamscale.jacoco.agent.util.AgentUtils
+import com.teamscale.report.util.ILogger
+import java.io.IOException
+import java.lang.instrument.Instrumentation
+import java.lang.management.ManagementFactory
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.Optional
+import kotlin.use
+
+/** Container class for the premain entry point for the agent. */
+object PreMain {
+ private lateinit var loggingResources: LoggingUtils.LoggingResources
+
+ /**
+ * System property that we use to prevent this agent from being attached to the same VM twice. This can happen if
+ * the agent is registered via multiple JVM environment variables and/or the command line at the same time.
+ */
+ private const val LOCKING_SYSTEM_PROPERTY = "TEAMSCALE_JAVA_PROFILER_ATTACHED"
+
+ /**
+ * Environment variable from which to read the config ID to use. This is an ID for a profiler configuration that is
+ * stored in Teamscale.
+ */
+ private const val CONFIG_ID_ENVIRONMENT_VARIABLE = "TEAMSCALE_JAVA_PROFILER_CONFIG_ID"
+
+ /** Environment variable from which to read the config file to use. */
+ private const val CONFIG_FILE_ENVIRONMENT_VARIABLE = "TEAMSCALE_JAVA_PROFILER_CONFIG_FILE"
+
+ /** Environment variable from which to read the Teamscale access token. */
+ private const val ACCESS_TOKEN_ENVIRONMENT_VARIABLE = "TEAMSCALE_ACCESS_TOKEN"
+
+ /**
+ * Entry point for the agent, called by the JVM.
+ */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun premain(options: String?, instrumentation: Instrumentation?) {
+ if (System.getProperty(LOCKING_SYSTEM_PROPERTY) != null) return
+ System.setProperty(LOCKING_SYSTEM_PROPERTY, "true")
+
+ val environmentConfigId = System.getenv(CONFIG_ID_ENVIRONMENT_VARIABLE)
+ val environmentConfigFile = System.getenv(CONFIG_FILE_ENVIRONMENT_VARIABLE)
+ if (StringUtils.isEmpty(options) && environmentConfigId == null && environmentConfigFile == null) {
+ // profiler was registered globally, and no config was set explicitly by the user, thus ignore this process
+ // and don't profile anything
+ return
+ }
+
+ var agentOptions: AgentOptions? = null
+ try {
+ val parseResult = getAndApplyAgentOptions(
+ options, environmentConfigId, environmentConfigFile
+ )
+ agentOptions = parseResult.first
+
+ // After parsing everything and configuring logging, we now
+ // can throw the caught exceptions.
+ parseResult.second?.forEach { exception ->
+ throw exception
+ }
+ } catch (e: AgentOptionParseException) {
+ LoggingUtils.loggerContext.getLogger(PreMain::class.java).error(e.message, e)
+
+ // Flush logs to Teamscale, if configured.
+ closeLoggingResources()
+
+ // Unregister the profiler from Teamscale.
+ agentOptions?.configurationViaTeamscale?.unregisterProfiler()
+
+ throw e
+ } catch (_: AgentOptionReceiveException) {
+ // When Teamscale is not available, we don't want to fail hard to still allow for testing even if no
+ // coverage is collected (see TS-33237)
+ return
+ }
+
+ val logger = LoggingUtils.getLogger(Agent::class.java)
+
+ logger.info("Teamscale Java profiler version ${AgentUtils.VERSION}")
+ logger.info("Starting JaCoCo's agent")
+ val agentBuilder = JacocoAgentOptionsBuilder(agentOptions)
+ JaCoCoPreMain.premain(agentBuilder.createJacocoAgentOptions(), instrumentation, logger)
+
+ agentOptions.configurationViaTeamscale?.startHeartbeatThreadAndRegisterShutdownHook()
+ createAgent(agentOptions, instrumentation).registerShutdownHook()
+ }
+
+ @Throws(AgentOptionParseException::class, IOException::class, AgentOptionReceiveException::class)
+ private fun getAndApplyAgentOptions(
+ options: String?,
+ environmentConfigId: String?,
+ environmentConfigFile: String?
+ ): Pair?> {
+ val delayedLogger = DelayedLogger()
+ val javaAgents = ManagementFactory.getRuntimeMXBean().inputArguments
+ .filter { it.contains("-javaagent") }
+ // We allow multiple instances of the teamscale-jacoco-agent as we ensure with the #LOCKING_SYSTEM_PROPERTY to only use it once
+ val differentAgents = javaAgents.filter { !it.contains("teamscale-jacoco-agent.jar") }
+
+ if (!differentAgents.isEmpty()) {
+ delayedLogger.warn(
+ "Using multiple java agents could interfere with coverage recording: ${
+ differentAgents.joinToString()
+ }"
+ )
+ }
+ if (!javaAgents.first().contains("teamscale-jacoco-agent.jar")) {
+ delayedLogger.warn("For best results consider registering the Teamscale Java Profiler first.")
+ }
+
+ val credentials = TeamscalePropertiesUtils.parseCredentials()
+ if (credentials == null) {
+ // As many users still don't use the installer based setup, this log message will be shown in almost every log.
+ // We use a debug log, as this message can be confusing for customers that think a teamscale.properties file is synonymous with a config file.
+ delayedLogger.debug(
+ "No explicit teamscale.properties file given. Looking for Teamscale credentials in a config file or via a command line argument. This is expected unless the installer based setup was used."
+ )
+ }
+
+ val environmentAccessToken = System.getenv(ACCESS_TOKEN_ENVIRONMENT_VARIABLE)
+
+ val parseResult: Pair>
+ val agentOptions: AgentOptions
+ try {
+ parseResult = AgentOptionsParser.parse(
+ options, environmentConfigId, environmentConfigFile, credentials, environmentAccessToken, delayedLogger
+ )
+ agentOptions = parseResult.first
+ } catch (e: AgentOptionParseException) {
+ initializeFallbackLogging(options, delayedLogger).use { _ ->
+ delayedLogger.errorAndStdErr("Failed to parse agent options: ${e.message}", e)
+ attemptLogAndThrow(delayedLogger)
+ throw e
+ }
+ } catch (e: AgentOptionReceiveException) {
+ initializeFallbackLogging(options, delayedLogger).use { _ ->
+ delayedLogger.errorAndStdErr("${e.message} The application should start up normally, but NO coverage will be collected! Check the log file for details.", e)
+ attemptLogAndThrow(delayedLogger)
+ throw e
+ }
+ }
+
+ initializeLogging(agentOptions, delayedLogger)
+ val logger = LoggingUtils.getLogger(Agent::class.java)
+ delayedLogger.logTo(logger)
+ HttpUtils.setShouldValidateSsl(agentOptions.shouldValidateSsl())
+
+ return parseResult
+ }
+
+ private fun attemptLogAndThrow(delayedLogger: DelayedLogger) {
+ // We perform actual logging output after writing to console to
+ // ensure the console is reached even in case of logging issues
+ // (see TS-23151). We use the Agent class here (same as below)
+ val logger = LoggingUtils.getLogger(Agent::class.java)
+ delayedLogger.logTo(logger)
+ }
+
+ /** Initializes logging during [premain] and also logs the log directory. */
+ @Throws(IOException::class)
+ private fun initializeLogging(agentOptions: AgentOptions, logger: DelayedLogger) {
+ if (agentOptions.isDebugLogging) {
+ initializeDebugLogging(agentOptions, logger)
+ } else {
+ loggingResources = LoggingUtils.initializeLogging(agentOptions.getLoggingConfig())
+ logger.info("Logging to ${LogDirectoryPropertyDefiner().getPropertyValue()}")
+ }
+
+ if (agentOptions.teamscaleServerOptions.isConfiguredForServerConnection) {
+ if (LogToTeamscaleAppender.addTeamscaleAppenderTo(LoggingUtils.loggerContext, agentOptions)) {
+ logger.info("Logs are being forwarded to Teamscale at ${agentOptions.teamscaleServerOptions.url}")
+ }
+ }
+ }
+
+ /** Closes the opened logging contexts. */
+ fun closeLoggingResources() {
+ loggingResources.close()
+ }
+
+ /**
+ * Returns in instance of the agent that was configured. Either an agent with interval based line-coverage dump or
+ * the HTTP server is used.
+ */
+ @Throws(UploaderException::class, IOException::class)
+ private fun createAgent(
+ agentOptions: AgentOptions,
+ instrumentation: Instrumentation?
+ ): AgentBase = if (agentOptions.useTestwiseCoverageMode()) {
+ TestwiseCoverageAgent.create(agentOptions)
+ } else {
+ Agent(agentOptions, instrumentation)
+ }
+
+ /**
+ * Initializes debug logging during [.premain] and also logs the log directory if
+ * given.
+ */
+ private fun initializeDebugLogging(agentOptions: AgentOptions, logger: DelayedLogger) {
+ loggingResources = LoggingUtils.initializeDebugLogging(agentOptions.getDebugLogDirectory())
+ val logDirectory = Paths.get(DebugLogDirectoryPropertyDefiner().getPropertyValue())
+ if (FileSystemUtils.isValidPath(logDirectory.toString()) && Files.isWritable(logDirectory)) {
+ logger.info("Logging to $logDirectory")
+ } else {
+ logger.warn("Could not create $logDirectory. Logging to console only.")
+ }
+ }
+
+ /**
+ * Initializes fallback logging in case of an error during the parsing of the options to
+ * [premain] (see TS-23151). This tries to extract the logging configuration and use
+ * this and falls back to the default logger.
+ */
+ private fun initializeFallbackLogging(
+ premainOptions: String?,
+ delayedLogger: DelayedLogger
+ ): LoggingUtils.LoggingResources? {
+ if (premainOptions == null) {
+ return LoggingUtils.initializeDefaultLogging()
+ }
+ premainOptions
+ .split(",".toRegex())
+ .dropLastWhile { it.isEmpty() }
+ .forEach { optionPart ->
+ if (optionPart.startsWith(AgentOptionsParser.DEBUG + "=")) {
+ val value = optionPart.split("=".toRegex(), limit = 2)[1]
+ val debugDisabled = value.equals("false", ignoreCase = true)
+ val debugEnabled = value.equals("true", ignoreCase = true)
+ if (debugDisabled) return@forEach
+ var debugLogDirectory: Path? = null
+ if (!value.isEmpty() && !debugEnabled) {
+ debugLogDirectory = Paths.get(value)
+ }
+ return LoggingUtils.initializeDebugLogging(debugLogDirectory)
+ }
+ if (optionPart.startsWith(AgentOptionsParser.LOGGING_CONFIG_OPTION + "=")) {
+ return createFallbackLoggerFromConfig(
+ optionPart.split("=".toRegex(), limit = 2)[1],
+ delayedLogger
+ )
+ }
+
+ if (optionPart.startsWith(AgentOptionsParser.CONFIG_FILE_OPTION + "=")) {
+ val configFileValue = optionPart.split("=".toRegex(), limit = 2)[1]
+ var loggingConfigLine: String? = null
+ try {
+ val configFile = FilePatternResolver(delayedLogger).parsePath(
+ AgentOptionsParser.CONFIG_FILE_OPTION, configFileValue
+ ).toFile()
+ loggingConfigLine = FileSystemUtils.readLinesUTF8(configFile)
+ .firstOrNull { it.startsWith(AgentOptionsParser.LOGGING_CONFIG_OPTION + "=") }
+ } catch (e: IOException) {
+ delayedLogger.error("Failed to load configuration from $configFileValue: ${e.message}", e)
+ }
+ loggingConfigLine?.let { config ->
+ return createFallbackLoggerFromConfig(
+ config.split("=".toRegex(), limit = 2)[1], delayedLogger
+ )
+ }
+ }
+ }
+
+ return LoggingUtils.initializeDefaultLogging()
+ }
+
+ /** Creates a fallback logger using the given config file. */
+ private fun createFallbackLoggerFromConfig(
+ configLocation: String,
+ delayedLogger: ILogger
+ ): LoggingUtils.LoggingResources {
+ try {
+ return LoggingUtils.initializeLogging(
+ FilePatternResolver(delayedLogger).parsePath(
+ AgentOptionsParser.LOGGING_CONFIG_OPTION,
+ configLocation
+ )
+ )
+ } catch (e: IOException) {
+ val message = "Failed to load log configuration from location $configLocation: ${e.message}"
+ delayedLogger.error(message, e)
+ // output the message to console as well, as this might
+ // otherwise not make it to the user
+ System.err.println(message)
+ return LoggingUtils.initializeDefaultLogging()
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/ResourceBase.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/ResourceBase.kt
new file mode 100644
index 000000000..fd4bb77cc
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/ResourceBase.kt
@@ -0,0 +1,141 @@
+package com.teamscale.jacoco.agent
+
+import com.teamscale.client.CommitDescriptor
+import com.teamscale.client.StringUtils
+import com.teamscale.client.TeamscaleServer
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.report.testwise.model.RevisionInfo
+import org.jetbrains.annotations.Contract
+import org.slf4j.Logger
+import java.util.Optional
+import javax.ws.rs.BadRequestException
+import javax.ws.rs.GET
+import javax.ws.rs.PUT
+import javax.ws.rs.Path
+import javax.ws.rs.Produces
+import javax.ws.rs.core.MediaType
+import javax.ws.rs.core.Response
+
+/**
+ * The resource of the Jersey + Jetty http server holding all the endpoints specific for the [AgentBase].
+ */
+abstract class ResourceBase {
+ /** The logger. */
+ @JvmField
+ protected val logger: Logger = LoggingUtils.getLogger(this)
+
+ companion object {
+ /**
+ * The agentBase inject via [AgentResource.setAgent] or
+ * [com.teamscale.jacoco.agent.testimpact.TestwiseCoverageResource.setAgent].
+ */
+ @JvmStatic
+ protected lateinit var agentBase: AgentBase
+ }
+
+ @get:Path("/partition")
+ @get:GET
+ val partition: String
+ /** Returns the partition for the Teamscale upload. */
+ get() = agentBase.options.teamscaleServerOptions.partition.orEmpty()
+
+ @get:Path("/message")
+ @get:GET
+ val message: String
+ /** Returns the upload message for the Teamscale upload. */
+ get() = agentBase.options.teamscaleServerOptions.message.orEmpty()
+
+ @get:Produces(MediaType.APPLICATION_JSON)
+ @get:Path("/revision")
+ @get:GET
+ val revision: RevisionInfo
+ /** Returns revision information for the Teamscale upload. */
+ get() = revisionInfo
+
+ @get:Produces(MediaType.APPLICATION_JSON)
+ @get:Path("/commit")
+ @get:GET
+ val commit: RevisionInfo
+ /** Returns revision information for the Teamscale upload. */
+ get() = revisionInfo
+
+ /** Handles setting the partition name. */
+ @PUT
+ @Path("/partition")
+ fun setPartition(partitionString: String): Response {
+ val partition = StringUtils.removeDoubleQuotes(partitionString)
+ if (partition.isEmpty()) {
+ handleBadRequest("The new partition name is missing in the request body! Please add it as plain text.")
+ }
+
+ logger.debug("Changing partition name to $partition")
+ agentBase.dumpReport()
+ agentBase.controller.sessionId = partition
+ agentBase.options.teamscaleServerOptions.partition = partition
+ return Response.noContent().build()
+ }
+
+ /** Handles setting the upload message. */
+ @PUT
+ @Path("/message")
+ fun setMessage(messageString: String): Response {
+ val message = StringUtils.removeDoubleQuotes(messageString)
+ if (message.isEmpty()) {
+ handleBadRequest("The new message is missing in the request body! Please add it as plain text.")
+ }
+
+ agentBase.dumpReport()
+ logger.debug("Changing message to $message")
+ agentBase.options.teamscaleServerOptions.message = message
+
+ return Response.noContent().build()
+ }
+
+ /** Handles setting the revision. */
+ @PUT
+ @Path("/revision")
+ fun setRevision(revisionString: String): Response {
+ val revision = StringUtils.removeDoubleQuotes(revisionString)
+ if (revision.isEmpty()) {
+ handleBadRequest("The new revision name is missing in the request body! Please add it as plain text.")
+ }
+
+ agentBase.dumpReport()
+ logger.debug("Changing revision name to $revision")
+ agentBase.options.teamscaleServerOptions.revision = revision
+
+ return Response.noContent().build()
+ }
+
+ /** Handles setting the upload commit. */
+ @PUT
+ @Path("/commit")
+ fun setCommit(commitString: String): Response {
+ val commit = StringUtils.removeDoubleQuotes(commitString)
+ if (commit.isEmpty()) {
+ handleBadRequest("The new upload commit is missing in the request body! Please add it as plain text.")
+ }
+
+ agentBase.dumpReport()
+ agentBase.options.teamscaleServerOptions.commit = CommitDescriptor.parse(commit)
+
+ return Response.noContent().build()
+ }
+
+ private val revisionInfo: RevisionInfo
+ /** Returns revision information for the Teamscale upload. */
+ get() {
+ val server = agentBase.options.teamscaleServerOptions
+ return RevisionInfo(server.commit, server.revision)
+ }
+
+ /**
+ * Handles bad requests to the endpoints.
+ */
+ @Contract(value = "_ -> fail")
+ @Throws(BadRequestException::class)
+ protected fun handleBadRequest(message: String?) {
+ logger.error(message)
+ throw BadRequestException(message)
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/commandline/ICommand.java b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/ICommand.kt
similarity index 77%
rename from agent/src/main/java/com/teamscale/jacoco/agent/commandline/ICommand.java
rename to agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/ICommand.kt
index 4ce2fa697..5bbc9b7cd 100644
--- a/agent/src/main/java/com/teamscale/jacoco/agent/commandline/ICommand.java
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/ICommand.kt
@@ -3,27 +3,26 @@
| Copyright (c) 2009-2017 CQSE GmbH |
| |
+-------------------------------------------------------------------------*/
-package com.teamscale.jacoco.agent.commandline;
+package com.teamscale.jacoco.agent.commandline
-import com.teamscale.jacoco.agent.options.AgentOptionParseException;
-
-import java.io.IOException;
+import com.teamscale.jacoco.agent.options.AgentOptionParseException
+import java.io.IOException
/**
* Interface for commands: argument parsing and execution.
*/
-public interface ICommand {
-
+interface ICommand {
/**
* Makes sure the arguments are valid. Must return all detected problems in the
* form of a user-visible message.
*/
- Validator validate() throws AgentOptionParseException, IOException;
+ @Throws(AgentOptionParseException::class, IOException::class)
+ fun validate(): Validator
/**
* Runs the implementation of the command. May throw an exception to indicate
* abnormal termination of the program.
*/
- void run() throws Exception;
-
+ @Throws(Exception::class)
+ fun run()
}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/Validator.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/Validator.kt
new file mode 100644
index 000000000..6f520dc7d
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/commandline/Validator.kt
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------+
+| |
+| Copyright (c) 2009-2017 CQSE GmbH |
+| |
++-------------------------------------------------------------------------*/
+package com.teamscale.jacoco.agent.commandline
+
+import com.teamscale.client.StringUtils
+import com.teamscale.jacoco.agent.util.Assertions
+
+/**
+ * Helper class to allow for multiple validations to occur.
+ */
+class Validator {
+ /** The found validation problems in the form of error messages for the user. */
+ private val messages = mutableListOf()
+
+ /** Runs the given validation routine. */
+ fun ensure(validation: ExceptionBasedValidation) {
+ try {
+ validation.validate()
+ } catch (e: Exception) {
+ e.message?.let { messages.add(it) }
+ } catch (e: AssertionError) {
+ e.message?.let { messages.add(it) }
+ }
+ }
+
+ /**
+ * Interface for a validation routine that throws an exception when it fails.
+ */
+ fun interface ExceptionBasedValidation {
+ /**
+ * Throws an [Exception] or [AssertionError] if the validation fails.
+ */
+ @Throws(Exception::class, AssertionError::class)
+ fun validate()
+ }
+
+ /**
+ * Checks that the given condition is `true` or adds the given error message.
+ */
+ fun isTrue(condition: Boolean, message: String?) {
+ ensure { Assertions.isTrue(condition, message) }
+ }
+
+ /**
+ * Checks that the given condition is `false` or adds the given error message.
+ */
+ fun isFalse(condition: Boolean, message: String?) {
+ ensure { Assertions.isFalse(condition, message) }
+ }
+
+ val isValid: Boolean
+ /** Returns `true` if the validation succeeded. */
+ get() = messages.isEmpty()
+
+ val errorMessage: String
+ /** Returns an error message with all validation problems that were found. */
+ get() = "- ${messages.joinToString("${StringUtils.LINE_FEED}- ")}"
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.kt
new file mode 100644
index 000000000..40c0a3104
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/AgentOptionReceiveException.kt
@@ -0,0 +1,7 @@
+package com.teamscale.jacoco.agent.configuration
+
+/** Thrown when retrieving the profiler configuration from Teamscale fails. */
+class AgentOptionReceiveException : Exception {
+ constructor(message: String?) : super(message)
+ constructor(message: String?, cause: Throwable?) : super(message, cause)
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.kt
new file mode 100644
index 000000000..dcea6074e
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.kt
@@ -0,0 +1,162 @@
+package com.teamscale.jacoco.agent.configuration
+
+import com.fasterxml.jackson.core.JsonProcessingException
+import com.teamscale.client.ITeamscaleService
+import com.teamscale.client.JsonUtils
+import com.teamscale.client.ProcessInformation
+import com.teamscale.client.ProfilerConfiguration
+import com.teamscale.client.ProfilerInfo
+import com.teamscale.client.ProfilerRegistration
+import com.teamscale.client.TeamscaleServiceGenerator
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.util.AgentUtils
+import com.teamscale.report.util.ILogger
+import okhttp3.HttpUrl
+import okhttp3.ResponseBody
+import retrofit2.Response
+import java.io.IOException
+import java.time.Duration
+import java.util.concurrent.Executors
+import java.util.concurrent.ThreadFactory
+import java.util.concurrent.TimeUnit
+
+/**
+ * Responsible for holding the configuration retrieved from Teamscale and sending regular heartbeat events to
+ * keep the profiler information in Teamscale up to date.
+ */
+class ConfigurationViaTeamscale(
+ private val teamscaleClient: ITeamscaleService,
+ profilerRegistration: ProfilerRegistration,
+ processInformation: ProcessInformation
+) {
+ /**
+ * The UUID that Teamscale assigned to this instance of the profiler during the registration. This ID needs to be
+ * used when communicating with Teamscale.
+ */
+ @JvmField
+ val profilerId = profilerRegistration.profilerId
+
+ private val profilerInfo = ProfilerInfo(processInformation, profilerRegistration.profilerConfiguration)
+
+ /** Returns the profiler configuration retrieved from Teamscale. */
+ val profilerConfiguration: ProfilerConfiguration?
+ get() = profilerInfo.profilerConfiguration
+
+ /**
+ * Starts a heartbeat thread and registers a shutdown hook.
+ *
+ *
+ * This spawns a new thread every minute which sends a heartbeat to Teamscale. It also registers a shutdown hook
+ * that unregisters the profiler from Teamscale.
+ */
+ fun startHeartbeatThreadAndRegisterShutdownHook() {
+ val executor = Executors.newSingleThreadScheduledExecutor { runnable ->
+ val thread = Thread(runnable)
+ thread.setDaemon(true)
+ thread
+ }
+
+ executor.scheduleAtFixedRate({ sendHeartbeat() }, 1, 1, TimeUnit.MINUTES)
+
+ Runtime.getRuntime().addShutdownHook(Thread {
+ executor.shutdownNow()
+ unregisterProfiler()
+ })
+ }
+
+ private fun sendHeartbeat() {
+ try {
+ val response = teamscaleClient.sendHeartbeat(profilerId!!, profilerInfo).execute()
+ if (!response.isSuccessful) {
+ LoggingUtils.getLogger(this)
+ .error("Failed to send heartbeat. Teamscale responded with: ${response.errorBody()?.string()}")
+ }
+ } catch (e: IOException) {
+ LoggingUtils.getLogger(this).error("Failed to send heartbeat to Teamscale!", e)
+ }
+ }
+
+ /** Unregisters the profiler in Teamscale (marks it as shut down). */
+ fun unregisterProfiler() {
+ try {
+ var response = teamscaleClient.unregisterProfiler(profilerId!!).execute()
+ if (response.code() == 405) {
+ response = teamscaleClient.unregisterProfilerLegacy(profilerId).execute()
+ }
+ if (!response.isSuccessful) {
+ LoggingUtils.getLogger(this)
+ .error("Failed to unregister profiler. Teamscale responded with: ${response.errorBody()?.string()}")
+ }
+ } catch (e: IOException) {
+ LoggingUtils.getLogger(this).error("Failed to unregister profiler!", e)
+ }
+ }
+
+ companion object {
+ /**
+ * Two minute timeout. This is quite high to account for an eventual high load on the Teamscale server. This is a
+ * tradeoff between fast application startup and potentially missing test coverage.
+ */
+ private val LONG_TIMEOUT: Duration = Duration.ofMinutes(2)
+
+ /**
+ * Tries to retrieve the profiler configuration from Teamscale. In case retrieval fails the method throws a
+ * [AgentOptionReceiveException].
+ */
+ @JvmStatic
+ @Throws(AgentOptionReceiveException::class)
+ fun retrieve(
+ logger: ILogger,
+ configurationId: String?,
+ url: HttpUrl,
+ userName: String,
+ userAccessToken: String
+ ): ConfigurationViaTeamscale {
+ val teamscaleClient = TeamscaleServiceGenerator
+ .createService(ITeamscaleService::class.java, url, userName, userAccessToken, AgentUtils.USER_AGENT, LONG_TIMEOUT, LONG_TIMEOUT)
+ try {
+ val processInformation = ProcessInformationRetriever(logger).processInformation
+ val response = teamscaleClient.registerProfiler(
+ configurationId,
+ processInformation
+ ).execute()
+ if (!response.isSuccessful) {
+ throw AgentOptionReceiveException(
+ "Failed to retrieve profiler configuration from Teamscale due to failed request. Http status: ${response.code()} Body: ${response.errorBody()?.string()}"
+ )
+ }
+
+ val body = response.body()
+ return parseProfilerRegistration(body!!, response, teamscaleClient, processInformation)
+ } catch (e: IOException) {
+ // we include the causing error message in this exception's message since this causes it to be printed
+ // to stderr which is much more helpful than just saying "something didn't work"
+ throw AgentOptionReceiveException(
+ "Failed to retrieve profiler configuration from Teamscale due to network error: ${
+ LoggingUtils.getStackTraceAsString(e)
+ }", e
+ )
+ }
+ }
+
+ @Throws(AgentOptionReceiveException::class, IOException::class)
+ private fun parseProfilerRegistration(
+ body: ResponseBody,
+ response: Response,
+ teamscaleClient: ITeamscaleService,
+ processInformation: ProcessInformation
+ ): ConfigurationViaTeamscale {
+ // We may only call this once
+ val bodyString = body.string()
+ try {
+ val registration = JsonUtils.deserialize(bodyString)
+ return ConfigurationViaTeamscale(teamscaleClient, registration, processInformation)
+ } catch (e: JsonProcessingException) {
+ throw AgentOptionReceiveException(
+ "Failed to retrieve profiler configuration from Teamscale due to invalid JSON. HTTP code: " + response.code() + " Response: " + bodyString,
+ e
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.kt
new file mode 100644
index 000000000..e4692acdd
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/configuration/ProcessInformationRetriever.kt
@@ -0,0 +1,53 @@
+package com.teamscale.jacoco.agent.configuration
+
+import com.teamscale.client.ProcessInformation
+import com.teamscale.report.util.ILogger
+import java.lang.management.ManagementFactory
+import java.net.InetAddress
+import java.net.UnknownHostException
+
+/**
+ * Is responsible for retrieving process information such as the host name and process ID.
+ */
+class ProcessInformationRetriever(private val logger: ILogger) {
+ /**
+ * Retrieves the process information, including the host name and process ID.
+ */
+ val processInformation: ProcessInformation
+ get() = ProcessInformation(hostName, pID, System.currentTimeMillis())
+
+ /**
+ * Retrieves the host name of the local machine.
+ */
+ private val hostName: String
+ get() {
+ try {
+ return InetAddress.getLocalHost().hostName
+ } catch (e: UnknownHostException) {
+ logger.error("Failed to determine hostname!", e)
+ return ""
+ }
+ }
+
+ /**
+ * Returns a string that *probably* contains the PID.
+ *
+ * On Java 9 there is an API to get the PID. But since we support Java 8, we may fall back to an undocumented API
+ * that at least contains the PID in most JVMs.
+ *
+ * See [This StackOverflow question](https://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-process-id)
+ */
+ companion object {
+ val pID: String
+ get() {
+ try {
+ val processHandleClass = Class.forName("java.lang.ProcessHandle")
+ val processHandle = processHandleClass.getMethod("current").invoke(null)
+ val pid = processHandleClass.getMethod("pid").invoke(processHandle) as Long
+ return pid.toString()
+ } catch (_: ReflectiveOperationException) {
+ return ManagementFactory.getRuntimeMXBean().name
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt
new file mode 100644
index 000000000..2183de6f5
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/ConvertCommand.kt
@@ -0,0 +1,156 @@
+/*-------------------------------------------------------------------------+
+| |
+| Copyright (c) 2009-2017 CQSE GmbH |
+| |
++-------------------------------------------------------------------------*/
+package com.teamscale.jacoco.agent.convert
+
+import com.beust.jcommander.Parameter
+import com.beust.jcommander.Parameters
+import com.teamscale.client.FileSystemUtils.ensureDirectoryExists
+import com.teamscale.client.StringUtils.isEmpty
+import com.teamscale.jacoco.agent.commandline.ICommand
+import com.teamscale.jacoco.agent.commandline.Validator
+import com.teamscale.jacoco.agent.options.ClasspathUtils
+import com.teamscale.jacoco.agent.options.FilePatternResolver
+import com.teamscale.jacoco.agent.util.Assertions
+import com.teamscale.report.EDuplicateClassFileBehavior
+import com.teamscale.report.util.CommandLineLogger
+import java.io.File
+import java.io.IOException
+
+/**
+ * Encapsulates all command line options for the convert command for parsing with [JCommander].
+ */
+@Parameters(
+ commandNames = ["convert"], commandDescription = "Converts a binary .exec coverage file to XML. " +
+ "Note that the XML report will only contain source file coverage information, but no class coverage."
+)
+class ConvertCommand : ICommand {
+ /** The directories and/or zips that contain all class files being profiled. */
+ @JvmField
+ @Parameter(
+ names = ["--class-dir", "--jar", "-c"], required = true, description = (""
+ + "The directories or zip/ear/jar/war/... files that contain the compiled Java classes being profiled."
+ + " Searches recursively, including inside zips. You may also supply a *.txt file with one path per line.")
+ )
+ var classDirectoriesOrZips = mutableListOf()
+
+ /**
+ * Wildcard include patterns to apply during JaCoCo's traversal of class files.
+ */
+ @Parameter(
+ names = ["--includes"], description = (""
+ + "Wildcard include patterns to apply to all found class file locations during JaCoCo's traversal of class files."
+ + " Note that zip contents are separated from zip files with @ and that you can filter only"
+ + " class files, not intermediate folders/zips. Use with great care as missing class files"
+ + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered."
+ + " Defaults to no filtering. Excludes overrule includes.")
+ )
+ var locationIncludeFilters = mutableListOf()
+
+ /**
+ * Wildcard exclude patterns to apply during JaCoCo's traversal of class files.
+ */
+ @Parameter(
+ names = ["--excludes", "-e"], description = (""
+ + "Wildcard exclude patterns to apply to all found class file locations during JaCoCo's traversal of class files."
+ + " Note that zip contents are separated from zip files with @ and that you can filter only"
+ + " class files, not intermediate folders/zips. Use with great care as missing class files"
+ + " lead to broken coverage files! Turn on debug logging to see which locations are being filtered."
+ + " Defaults to no filtering. Excludes overrule includes.")
+ )
+ var locationExcludeFilters = mutableListOf()
+
+ /** The directory to write the XML traces to. */
+ @JvmField
+ @Parameter(
+ names = ["--in", "-i"], required = true, description = ("" + "The binary .exec file(s), test details and " +
+ "test executions to read. Can be a single file or a directory that is recursively scanned for relevant files.")
+ )
+ var inputFiles = mutableListOf()
+
+ /** The directory to write the XML traces to. */
+ @JvmField
+ @Parameter(
+ names = ["--out", "-o"], required = true, description = (""
+ + "The file to write the generated XML report to.")
+ )
+ var outputFile = ""
+
+ /** Whether to ignore duplicate, non-identical class files. */
+ @Parameter(
+ names = ["--duplicates", "-d"], arity = 1, description = (""
+ + "Whether to ignore duplicate, non-identical class files."
+ + " This is discouraged and may result in incorrect coverage files. Defaults to WARN. " +
+ "Options are FAIL, WARN and IGNORE.")
+ )
+ var duplicateClassFileBehavior = EDuplicateClassFileBehavior.WARN
+
+ /** Whether to ignore uncovered class files. */
+ @Parameter(
+ names = ["--ignore-uncovered-classes"], required = false, arity = 1, description = (""
+ + "Whether to ignore uncovered classes."
+ + " These classes will not be part of the XML report at all, making it considerably smaller in some cases. Defaults to false.")
+ )
+ var shouldIgnoreUncoveredClasses = false
+
+ /** Whether testwise coverage or jacoco coverage should be generated. */
+ @Parameter(
+ names = ["--testwise-coverage", "-t"], required = false, arity = 0, description = "Whether testwise " +
+ "coverage or jacoco coverage should be generated."
+ )
+ var shouldGenerateTestwiseCoverage = false
+
+ /** After how many tests testwise coverage should be split into multiple reports. */
+ @Parameter(
+ names = ["--split-after", "-s"], required = false, arity = 1, description = "After how many tests " +
+ "testwise coverage should be split into multiple reports (Default is 5000)."
+ )
+ val splitAfter = 5000
+
+ @Throws(IOException::class)
+ fun getClassDirectoriesOrZips(): List = ClasspathUtils
+ .resolveClasspathTextFiles(
+ "class-dir", FilePatternResolver(CommandLineLogger()),
+ classDirectoriesOrZips
+ )
+
+ fun getInputFiles() = inputFiles.map { File(it) }
+ fun getOutputFile() = File(outputFile)
+
+ /** Makes sure the arguments are valid. */
+ override fun validate() = Validator().apply {
+ val classDirectoriesOrZips = mutableListOf()
+ ensure { classDirectoriesOrZips.addAll(getClassDirectoriesOrZips()) }
+ isFalse(
+ classDirectoriesOrZips.isEmpty(),
+ "You must specify at least one directory or zip that contains class files"
+ )
+ classDirectoriesOrZips.forEach { path ->
+ isTrue(path.exists(), "Path '$path' does not exist")
+ isTrue(path.canRead(), "Path '$path' is not readable")
+ }
+ getInputFiles().forEach { inputFile ->
+ isTrue(inputFile.exists() && inputFile.canRead(), "Cannot read the input file $inputFile")
+ }
+ ensure {
+ Assertions.isFalse(isEmpty(outputFile), "You must specify an output file")
+ val outputDir = getOutputFile().getAbsoluteFile().getParentFile()
+ ensureDirectoryExists(outputDir)
+ Assertions.isTrue(outputDir.canWrite(), "Path '$outputDir' is not writable")
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Throws(Exception::class)
+ override fun run() {
+ Converter(this).apply {
+ if (shouldGenerateTestwiseCoverage) {
+ runTestwiseCoverageReportGeneration()
+ } else {
+ runJaCoCoReportGeneration()
+ }
+ }
+ }
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/Converter.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/Converter.kt
new file mode 100644
index 000000000..0506d8acf
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/convert/Converter.kt
@@ -0,0 +1,99 @@
+package com.teamscale.jacoco.agent.convert
+
+import com.teamscale.client.TestDetails
+import com.teamscale.jacoco.agent.benchmark
+import com.teamscale.jacoco.agent.logging.LoggingUtils
+import com.teamscale.jacoco.agent.options.AgentOptionParseException
+import com.teamscale.jacoco.agent.util.Benchmark
+import com.teamscale.report.ReportUtils
+import com.teamscale.report.ReportUtils.listFiles
+import com.teamscale.report.jacoco.EmptyReportException
+import com.teamscale.report.jacoco.JaCoCoXmlReportGenerator
+import com.teamscale.report.testwise.ETestArtifactFormat
+import com.teamscale.report.testwise.TestwiseCoverageReportWriter
+import com.teamscale.report.testwise.jacoco.JaCoCoTestwiseReportGenerator
+import com.teamscale.report.testwise.model.TestExecution
+import com.teamscale.report.testwise.model.factory.TestInfoFactory
+import com.teamscale.report.util.ClasspathWildcardIncludeFilter
+import com.teamscale.report.util.CommandLineLogger
+import com.teamscale.report.util.ILogger
+import java.io.File
+import java.io.IOException
+import java.lang.String
+import java.nio.file.Paths
+import kotlin.Array
+import kotlin.Throws
+import kotlin.use
+
+/** Converts one .exec binary coverage file to XML. */
+class Converter
+/** Constructor. */(
+ /** The command line arguments. */
+ private val arguments: ConvertCommand
+) {
+ /** Converts one .exec binary coverage file to XML. */
+ @Throws(IOException::class)
+ fun runJaCoCoReportGeneration() {
+ val logger = LoggingUtils.getLogger(this)
+ val generator = JaCoCoXmlReportGenerator(
+ arguments.getClassDirectoriesOrZips(),
+ wildcardIncludeExcludeFilter,
+ arguments.duplicateClassFileBehavior,
+ arguments.shouldIgnoreUncoveredClasses,
+ LoggingUtils.wrap(logger)
+ )
+
+ val jacocoExecutionDataList = listFiles(ETestArtifactFormat.JACOCO, arguments.getInputFiles())
+ try {
+ benchmark("Generating the XML report") {
+ generator.convertExecFilesToReport(jacocoExecutionDataList, Paths.get(arguments.outputFile).toFile())
+ }
+ } catch (e: EmptyReportException) {
+ logger.warn("Converted report was empty.", e)
+ }
+ }
+
+ /** Converts one .exec binary coverage file, test details and test execution files to JSON testwise coverage. */
+ @Throws(IOException::class, AgentOptionParseException::class)
+ fun runTestwiseCoverageReportGeneration() {
+ val testDetails = ReportUtils.readObjects(
+ ETestArtifactFormat.TEST_LIST,
+ Array::class.java,
+ arguments.getInputFiles()
+ )
+ val testExecutions = ReportUtils.readObjects(
+ ETestArtifactFormat.TEST_EXECUTION,
+ Array::class.java,
+ arguments.getInputFiles()
+ )
+
+ val jacocoExecutionDataList = listFiles(ETestArtifactFormat.JACOCO, arguments.getInputFiles())
+ val logger = CommandLineLogger()
+
+ val generator = JaCoCoTestwiseReportGenerator(
+ arguments.getClassDirectoriesOrZips(),
+ this.wildcardIncludeExcludeFilter,
+ arguments.duplicateClassFileBehavior,
+ logger
+ )
+
+ benchmark("Generating the testwise coverage report") {
+ logger.info("Writing report with ${testDetails.size} Details/${testExecutions.size} Results")
+ TestwiseCoverageReportWriter(
+ TestInfoFactory(testDetails, testExecutions),
+ arguments.getOutputFile(),
+ arguments.splitAfter, null
+ ).use { coverageWriter ->
+ jacocoExecutionDataList.forEach { executionDataFile ->
+ generator.convertAndConsume(executionDataFile, coverageWriter)
+ }
+ }
+ }
+ }
+
+ private val wildcardIncludeExcludeFilter: ClasspathWildcardIncludeFilter
+ get() = ClasspathWildcardIncludeFilter(
+ String.join(":", arguments.locationIncludeFilters),
+ String.join(":", arguments.locationExcludeFilters)
+ )
+}
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.kt
new file mode 100644
index 000000000..876e989f2
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/DebugLogDirectoryPropertyDefiner.kt
@@ -0,0 +1,14 @@
+package com.teamscale.jacoco.agent.logging
+
+import java.nio.file.Path
+
+/** Defines a property that contains the path to which log files should be written. */
+class DebugLogDirectoryPropertyDefiner : LogDirectoryPropertyDefiner() {
+ override fun getPropertyValue() =
+ filePath?.resolve("logs")?.toAbsolutePath()?.toString() ?: super.getPropertyValue()
+
+ companion object {
+ /** File path for debug logging. */ /* package */
+ var filePath: Path? = null
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.kt
new file mode 100644
index 000000000..c587a45f0
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogDirectoryPropertyDefiner.kt
@@ -0,0 +1,10 @@
+package com.teamscale.jacoco.agent.logging
+
+import ch.qos.logback.core.PropertyDefinerBase
+import com.teamscale.jacoco.agent.util.AgentUtils
+
+/** Defines a property that contains the default path to which log files should be written. */
+open class LogDirectoryPropertyDefiner : PropertyDefinerBase() {
+ override fun getPropertyValue() =
+ AgentUtils.mainTempDirectory.resolve("logs").toAbsolutePath().toString()
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.kt
new file mode 100644
index 000000000..f646f9848
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LogToTeamscaleAppender.kt
@@ -0,0 +1,183 @@
+package com.teamscale.jacoco.agent.logging
+
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.LoggerContext
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.AppenderBase
+import ch.qos.logback.core.status.ErrorStatus
+import com.teamscale.client.ITeamscaleService
+import com.teamscale.client.ProfilerLogEntry
+import com.teamscale.jacoco.agent.options.AgentOptions
+import java.net.ConnectException
+import java.time.Duration
+import java.util.Collections
+import java.util.IdentityHashMap
+import java.util.LinkedHashSet
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.function.BiConsumer
+
+/**
+ * Custom log appender that sends logs to Teamscale; it buffers log that were not sent due to connection issues and
+ * sends them later.
+ */
+class LogToTeamscaleAppender : AppenderBase() {
+ /** The unique ID of the profiler */
+ private var profilerId: String? = null
+
+ /**
+ * Buffer for unsent logs. We use a set here to allow for removing entries fast after sending them to Teamscale was
+ * successful.
+ */
+ private val logBuffer = LinkedHashSet()
+
+ /** Scheduler for sending logs after the configured time interval */
+ private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1) { r ->
+ // Make the thread a daemon so that it does not prevent the JVM from terminating.
+ val t = Executors.defaultThreadFactory().newThread(r)
+ t.setDaemon(true)
+ t
+ }
+
+ /** Active log flushing threads */
+ private val activeLogFlushes: MutableSet> =
+ Collections.newSetFromMap(IdentityHashMap())
+
+ /** Is there a flush going on right now? */
+ private val isFlusing = AtomicBoolean(false)
+
+ override fun start() {
+ super.start()
+ scheduler.scheduleAtFixedRate({
+ synchronized(activeLogFlushes) {
+ activeLogFlushes.removeIf { it.isDone }
+ if (activeLogFlushes.isEmpty()) flush()
+ }
+ }, FLUSH_INTERVAL.toMillis(), FLUSH_INTERVAL.toMillis(), TimeUnit.MILLISECONDS)
+ }
+
+ override fun append(eventObject: ILoggingEvent) {
+ synchronized(logBuffer) {
+ logBuffer.add(formatLog(eventObject))
+ if (logBuffer.size >= BATCH_SIZE) flush()
+ }
+ }
+
+ private fun formatLog(eventObject: ILoggingEvent): ProfilerLogEntry {
+ val trace = LoggingUtils.getStackTraceFromEvent(eventObject)
+ val timestamp = eventObject.timeStamp
+ val message = eventObject.formattedMessage
+ val severity = eventObject.level.toString()
+ return ProfilerLogEntry(timestamp, message, trace, severity)
+ }
+
+ private fun flush() {
+ sendLogs()
+ }
+
+ /** Send logs in a separate thread */
+ private fun sendLogs() {
+ synchronized(activeLogFlushes) {
+ activeLogFlushes.add(CompletableFuture.runAsync {
+ if (isFlusing.compareAndSet(false, true)) {
+ try {
+ val client = teamscaleClient ?: return@runAsync // There might be no connection configured.
+
+ val logsToSend: MutableList
+ synchronized(logBuffer) {
+ logsToSend = logBuffer.toMutableList()
+ }
+
+ val call = client.postProfilerLog(profilerId!!, logsToSend)
+ val response = call.execute()
+ check(response.isSuccessful) { "Failed to send log: HTTP error code : ${response.code()}" }
+
+ synchronized(logBuffer) {
+ // Removing the logs that have been sent after the fact.
+ // This handles problems with lost network connections.
+ logBuffer.removeAll(logsToSend.toSet())
+ }
+ } catch (e: Exception) {
+ // We do not report on exceptions here.
+ if (e !is ConnectException) {
+ addStatus(ErrorStatus("Sending logs to Teamscale failed: ${e.message}", this, e))
+ }
+ } finally {
+ isFlusing.set(false)
+ }
+ }
+ }.whenComplete(BiConsumer { _, _ ->
+ synchronized(activeLogFlushes) {
+ activeLogFlushes.removeIf { it.isDone }
+ }
+ }))
+ }
+ }
+
+ override fun stop() {
+ // Already flush here once to make sure that we do not miss too much.
+ flush()
+
+ scheduler.shutdown()
+ try {
+ if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
+ scheduler.shutdownNow()
+ }
+ } catch (_: InterruptedException) {
+ scheduler.shutdownNow()
+ }
+
+ // A final flush after the scheduler has been shut down.
+ flush()
+
+ // Block until all flushes are done
+ CompletableFuture.allOf(*activeLogFlushes.toTypedArray()).join()
+
+ super.stop()
+ }
+
+ fun setTeamscaleClient(teamscaleClient: ITeamscaleService?) {
+ Companion.teamscaleClient = teamscaleClient
+ }
+
+ fun setProfilerId(profilerId: String) {
+ this.profilerId = profilerId
+ }
+
+ companion object {
+ /** Flush the logs after N elements are in the queue */
+ private const val BATCH_SIZE = 50
+
+ /** Flush the logs in the given time interval */
+ private val FLUSH_INTERVAL: Duration = Duration.ofSeconds(3)
+
+ /** The service client for sending logs to Teamscale */
+ private var teamscaleClient: ITeamscaleService? = null
+
+ /**
+ * Add the [LogToTeamscaleAppender] to the logging configuration and
+ * enable/start it.
+ */
+ fun addTeamscaleAppenderTo(context: LoggerContext, agentOptions: AgentOptions): Boolean {
+ val client = agentOptions.createTeamscaleClient(false)
+ if (client == null || agentOptions.configurationViaTeamscale == null) {
+ return false
+ }
+
+ context.getLogger(Logger.ROOT_LOGGER_NAME).apply {
+ val logToTeamscaleAppender = LogToTeamscaleAppender().apply {
+ setContext(context)
+ setProfilerId(agentOptions.configurationViaTeamscale.profilerId!!)
+ setTeamscaleClient(client.service)
+ start()
+ }
+ addAppender(logToTeamscaleAppender)
+ }
+
+ return true
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LoggingUtils.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LoggingUtils.kt
new file mode 100644
index 000000000..0e0a244a9
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/logging/LoggingUtils.kt
@@ -0,0 +1,124 @@
+package com.teamscale.jacoco.agent.logging
+
+import ch.qos.logback.classic.LoggerContext
+import ch.qos.logback.classic.joran.JoranConfigurator
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.classic.spi.ThrowableProxy
+import ch.qos.logback.classic.spi.ThrowableProxyUtil
+import ch.qos.logback.core.joran.spi.JoranException
+import ch.qos.logback.core.util.StatusPrinter
+import com.teamscale.jacoco.agent.Agent
+import com.teamscale.jacoco.agent.util.NullOutputStream
+import com.teamscale.report.util.ILogger
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.FileInputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.PrintStream
+import java.lang.AutoCloseable
+import java.nio.file.Path
+
+/**
+ * Helps initialize the logging framework properly.
+ */
+object LoggingUtils {
+ /** Returns a logger for the given object's class. */
+ @JvmStatic
+ fun getLogger(obj: Any): Logger = LoggerFactory.getLogger(obj.javaClass)
+
+ /** Returns a logger for the given class. */
+ @JvmStatic
+ fun getLogger(obj: Class<*>): Logger = LoggerFactory.getLogger(obj)
+
+ /** Initializes the logging to the default configured in the Jar. */
+ fun initializeDefaultLogging(): LoggingResources {
+ val stream = Agent::class.java.getResourceAsStream("logback-default.xml")
+ reconfigureLoggerContext(stream)
+ return LoggingResources()
+ }
+
+ /**
+ * Returns the logger context.
+ */
+ val loggerContext: LoggerContext
+ get() = LoggerFactory.getILoggerFactory() as LoggerContext
+
+ /**
+ * Extracts the stack trace from an ILoggingEvent using ThrowableProxyUtil.
+ *
+ * @param event the logging event containing the exception
+ * @return the stack trace as a String, or null if no exception is associated
+ */
+ fun getStackTraceFromEvent(event: ILoggingEvent) =
+ event.throwableProxy?.let { ThrowableProxyUtil.asString(it) }
+
+ /**
+ * Converts a Throwable to its stack trace as a String.
+ *
+ * @param throwable the throwable to convert
+ * @return the stack trace as a String
+ */
+ @JvmStatic
+ fun getStackTraceAsString(throwable: Throwable?) =
+ throwable?.let { ThrowableProxyUtil.asString(ThrowableProxy(it)) }
+
+ /**
+ * Reconfigures the logger context to use the configuration XML from the given input stream. Cf. [https://logback.qos.ch/manual/configuration.html](https://logback.qos.ch/manual/configuration.html)
+ */
+ private fun reconfigureLoggerContext(stream: InputStream?) {
+ StatusPrinter.setPrintStream(PrintStream(NullOutputStream()))
+ try {
+ val configurator = JoranConfigurator()
+ configurator.setContext(loggerContext)
+ loggerContext.reset()
+ configurator.doConfigure(stream)
+ } catch (_: JoranException) {
+ // StatusPrinter will handle this
+ }
+ StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext)
+ }
+
+ /**
+ * Initializes the logging from the given file. If that is `null`, uses [ ][.initializeDefaultLogging] instead.
+ */
+ @Throws(IOException::class)
+ fun initializeLogging(loggingConfigFile: Path?): LoggingResources {
+ if (loggingConfigFile == null) {
+ return initializeDefaultLogging()
+ }
+
+ reconfigureLoggerContext(FileInputStream(loggingConfigFile.toFile()))
+ return LoggingResources()
+ }
+
+ /** Initializes debug logging. */
+ fun initializeDebugLogging(logDirectory: Path?): LoggingResources {
+ if (logDirectory != null) {
+ DebugLogDirectoryPropertyDefiner.filePath = logDirectory
+ }
+ val stream = Agent::class.java.getResourceAsStream("logback-default-debugging.xml")
+ reconfigureLoggerContext(stream)
+ return LoggingResources()
+ }
+
+ /** Wraps the given slf4j logger into an [com.teamscale.report.util.ILogger]. */
+ @JvmStatic
+ fun wrap(logger: Logger): ILogger {
+ return object : ILogger {
+ override fun debug(message: String) = logger.debug(message)
+ override fun info(message: String) = logger.info(message)
+ override fun warn(message: String) = logger.warn(message)
+ override fun warn(message: String, throwable: Throwable?) = logger.warn(message, throwable)
+ override fun error(throwable: Throwable) = logger.error(throwable.message, throwable)
+ override fun error(message: String, throwable: Throwable?) = logger.error(message, throwable)
+ }
+ }
+
+ /** Class to use with try-with-resources to close the logging framework's resources. */
+ class LoggingResources : AutoCloseable {
+ override fun close() {
+ loggerContext.stop()
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/AgentUtils.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/AgentUtils.kt
new file mode 100644
index 000000000..6f5eb0d84
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/AgentUtils.kt
@@ -0,0 +1,57 @@
+package com.teamscale.jacoco.agent.util
+
+import com.teamscale.client.FileSystemUtils
+import com.teamscale.client.TeamscaleServiceGenerator
+import com.teamscale.jacoco.agent.PreMain
+import com.teamscale.jacoco.agent.configuration.ProcessInformationRetriever
+import java.io.IOException
+import java.net.URISyntaxException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.*
+
+/** General utilities for working with the agent. */
+object AgentUtils {
+ /** Version of this program. */
+ val VERSION: String
+
+ /** User-Agent header value for HTTP requests. */
+ @JvmField
+ val USER_AGENT: String
+
+ /**
+ * Returns the main temporary directory where all agent temp files should be placed.
+ */
+ @JvmStatic
+ val mainTempDirectory: Path by lazy {
+ try {
+ // We add a trailing hyphen here to visually separate the PID from the random number that Java appends
+ // to the name to make it unique
+ Files.createTempDirectory(
+ "teamscale-java-profiler-${FileSystemUtils.toSafeFilename(ProcessInformationRetriever.pID)}-"
+ )
+ } catch (e: IOException) {
+ throw RuntimeException("Failed to create temporary directory for agent files", e)
+ }
+ }
+
+ /** Returns the directory that contains the agent installation. */
+ @JvmStatic
+ val agentDirectory: Path by lazy {
+ try {
+ val jarFileUri = PreMain::class.java.getProtectionDomain().codeSource.location.toURI()
+ // we assume that the dist zip is extracted and the agent jar not moved
+ val jarDirectory = Paths.get(jarFileUri).parent
+ jarDirectory.parent ?: jarDirectory // happens when the jar file is stored in the root directory
+ } catch (e: URISyntaxException) {
+ throw RuntimeException("Failed to obtain agent directory. This is a bug, please report it.", e)
+ }
+ }
+
+ init {
+ val bundle = ResourceBundle.getBundle("com.teamscale.jacoco.agent.app")
+ VERSION = bundle.getString("version")
+ USER_AGENT = TeamscaleServiceGenerator.buildUserAgent("Teamscale Java Profiler", VERSION)
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/Assertions.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/Assertions.kt
new file mode 100644
index 000000000..ce9b0e44f
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/Assertions.kt
@@ -0,0 +1,48 @@
+package com.teamscale.jacoco.agent.util
+
+import org.jetbrains.annotations.Contract
+
+/**
+ * Simple methods to implement assertions.
+ */
+object Assertions {
+ /**
+ * Checks if a condition is `true`.
+ *
+ * @param condition condition to check
+ * @param message exception message
+ * @throws AssertionError if the condition is `false`
+ */
+ @JvmStatic
+ @Contract(value = "false, _ -> fail", pure = true)
+ @Throws(AssertionError::class)
+ fun isTrue(condition: Boolean, message: String?) {
+ throwAssertionErrorIfTestFails(condition, message)
+ }
+
+ /**
+ * Checks if a condition is `false`.
+ *
+ * @param condition condition to check
+ * @param message exception message
+ * @throws AssertionError if the condition is `true`
+ */
+ @Contract(value = "true, _ -> fail", pure = true)
+ @Throws(AssertionError::class)
+ fun isFalse(condition: Boolean, message: String?) {
+ throwAssertionErrorIfTestFails(!condition, message)
+ }
+
+ /**
+ * Throws an [AssertionError] if the test fails.
+ *
+ * @param test test which should be true
+ * @param message exception message
+ * @throws AssertionError if the test fails
+ */
+ private fun throwAssertionErrorIfTestFails(test: Boolean, message: String?) {
+ if (!test) {
+ throw AssertionError(message)
+ }
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/DaemonThreadFactory.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/DaemonThreadFactory.kt
new file mode 100644
index 000000000..ac4beb65b
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/DaemonThreadFactory.kt
@@ -0,0 +1,15 @@
+package com.teamscale.jacoco.agent.util
+
+import java.util.concurrent.ThreadFactory
+
+/**
+ * [java.util.concurrent.ThreadFactory] that only produces deamon threads (threads that don't prevent JVM shutdown) with a fixed name.
+ */
+class DaemonThreadFactory(owningClass: Class<*>, threadName: String?) : ThreadFactory {
+ private val threadName = "Teamscale Java Profiler ${owningClass.getSimpleName()} $threadName"
+
+ override fun newThread(runnable: Runnable) =
+ Thread(runnable, threadName).apply {
+ setDaemon(true)
+ }
+}
\ No newline at end of file
diff --git a/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/NullOutputStream.kt b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/NullOutputStream.kt
new file mode 100644
index 000000000..6f15a8e6b
--- /dev/null
+++ b/agent/src/main/kotlin/com/teamscale/jacoco/agent/util/NullOutputStream.kt
@@ -0,0 +1,20 @@
+package com.teamscale.jacoco.agent.util
+
+import java.io.IOException
+import java.io.OutputStream
+
+/** NOP output stream implementation. */
+class NullOutputStream : OutputStream() {
+ override fun write(b: ByteArray, off: Int, len: Int) {
+ // to /dev/null
+ }
+
+ override fun write(b: Int) {
+ // to /dev/null
+ }
+
+ @Throws(IOException::class)
+ override fun write(b: ByteArray) {
+ // to /dev/null
+ }
+}
\ No newline at end of file
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt
index 314283eab..c7e52e018 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/ITeamscaleService.kt
@@ -159,7 +159,7 @@ interface ITeamscaleService {
@POST("api/v2024.7.0/profilers/{profilerId}/logs")
fun postProfilerLog(
@Path("profilerId") profilerId: String,
- @Body logEntries: List?
+ @Body logEntries: List?
): Call
}
diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt
index 64800f45d..3e2f1169d 100644
--- a/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt
+++ b/teamscale-client/src/main/kotlin/com/teamscale/client/StringUtils.kt
@@ -139,7 +139,7 @@ object StringUtils {
* list is returned.
*/
@JvmStatic
- fun splitLinesAsList(content: String?): List = content?.lines() ?: emptyList()
+ fun splitLinesAsList(content: String?) = content?.lines() ?: emptyList()
/**
* Test if a string ends with one of the provided suffixes. Returns
@@ -147,18 +147,14 @@ object StringUtils {
* for short lists of suffixes.
*/
@JvmStatic
- fun endsWithOneOf(string: String, vararg suffixes: String): Boolean {
- return suffixes.any { string.endsWith(it) }
- }
+ fun endsWithOneOf(string: String, vararg suffixes: String) = suffixes.any { string.endsWith(it) }
/**
* Removes double quotes from beginning and end (if present) and returns the new
* string.
*/
@JvmStatic
- fun removeDoubleQuotes(string: String): String {
- return string.removeSuffix("\"").removePrefix("\"")
- }
+ fun removeDoubleQuotes(string: String) = string.removeSuffix("\"").removePrefix("\"")
/**
* Converts an empty string to null. If the input string is not empty, it returns the string unmodified.
@@ -167,9 +163,7 @@ object StringUtils {
* @return `null` if the input string is empty after trimming; the original string otherwise.
*/
@JvmStatic
- fun emptyToNull(string: String): String? {
- return if (isEmpty(string)) null else string
- }
+ fun emptyToNull(string: String) = if (isEmpty(string)) null else string
/**
* Converts a nullable string to a non-null, empty string.
@@ -179,7 +173,5 @@ object StringUtils {
* @return a non-null string; either the original string or an empty string if the input was null
*/
@JvmStatic
- fun nullToEmpty(stringOrNull: String?): String {
- return stringOrNull ?: EMPTY_STRING
- }
+ fun nullToEmpty(stringOrNull: String?) = stringOrNull ?: EMPTY_STRING
}