3535
3636import static com .teamscale .jacoco .agent .logging .LoggingUtils .getLoggerContext ;
3737
38- /** Container class for the premain entry point for the agent. */
38+ /**
39+ * Container class for the premain entry point for the agent.
40+ * <p>
41+ * <b>Design decision: the agent must never crash the profiled application.</b>
42+ * Even when the agent is misconfigured, startup must not propagate any exception out of
43+ * {@link #premain(String, Instrumentation)}. An exception escaping {@code premain} would be re-thrown by the JVM as
44+ * {@code IllegalAgentException}, which aborts startup of the profiled application. The profiler is an observational
45+ * tool, so a failure to start up the profiler must degrade to "no coverage is collected" rather than taking down the
46+ * application. All failure paths in {@code premain} are therefore logged and swallowed; see the top-level
47+ * {@code try}/{@code catch} in {@link #premain(String, Instrumentation)}.
48+ */
3949public class PreMain {
4050
4151 private static LoggingUtils .LoggingResources loggingResources = null ;
@@ -60,8 +70,26 @@ public class PreMain {
6070
6171 /**
6272 * Entry point for the agent, called by the JVM.
73+ * <p>
74+ * This method never throws. Any exception raised during startup is logged (to the file log and, if configured,
75+ * to Teamscale) and swallowed, and the JVM continues without coverage collection. See the class-level Javadoc for
76+ * the rationale.
6377 */
64- public static void premain (String options , Instrumentation instrumentation ) throws Exception {
78+ public static void premain (String options , Instrumentation instrumentation ) {
79+ try {
80+ startProfiler (options , instrumentation );
81+ } catch (Throwable t ) {
82+ // Top-level safety net: an exception escaping premain would be re-thrown by the JVM as
83+ // IllegalAgentException and abort the profiled application. See class-level Javadoc.
84+ logStartupFailure (t );
85+ }
86+ }
87+
88+ /**
89+ * Starts the profiler. Contains all logic that may legitimately throw during startup; every caller must route
90+ * through {@link #premain(String, Instrumentation)} which catches these exceptions.
91+ */
92+ private static void startProfiler (String options , Instrumentation instrumentation ) throws Exception {
6593 if (System .getProperty (LOCKING_SYSTEM_PROPERTY ) != null ) {
6694 return ;
6795 }
@@ -81,8 +109,8 @@ public static void premain(String options, Instrumentation instrumentation) thro
81109 environmentConfigFile );
82110 agentOptions = parseResult .getFirst ();
83111
84- // After parsing everything and configuring logging, we now
85- // can throw the caught exceptions .
112+ // After parsing everything and configuring logging, we now can throw the caught exceptions. They are
113+ // handled locally below and never propagate out of premain (see class-level Javadoc) .
86114 for (Exception exception : parseResult .getSecond ()) {
87115 throw exception ;
88116 }
@@ -97,7 +125,9 @@ public static void premain(String options, Instrumentation instrumentation) thro
97125 agentOptions .configurationViaTeamscale .unregisterProfiler ();
98126 }
99127
100- throw e ;
128+ // Previously rethrown. We now swallow the exception so a misconfigured agent cannot abort the profiled
129+ // application. The error has been logged above.
130+ return ;
101131 } catch (AgentOptionReceiveException e ) {
102132 // When Teamscale is not available, we don't want to fail hard to still allow for testing even if no
103133 // coverage is collected (see TS-33237)
@@ -118,6 +148,29 @@ public static void premain(String options, Instrumentation instrumentation) thro
118148 agent .registerShutdownHook ();
119149 }
120150
151+ /**
152+ * Logs a startup failure on a best-effort basis. Must not throw: even logging failures are swallowed so that
153+ * {@link #premain(String, Instrumentation)} keeps its no-throw contract.
154+ */
155+ private static void logStartupFailure (Throwable t ) {
156+ try {
157+ LoggingUtils .getLogger (PreMain .class ).error (
158+ "Teamscale Java Profiler failed to start up. The profiled application will continue to run "
159+ + "without coverage collection." , t );
160+ } catch (Throwable loggingFailure ) {
161+ // The logger may not be initialized yet or may itself be broken. Fall back to stderr so the failure is
162+ // at least visible somewhere, then give up silently.
163+ try {
164+ System .err .println (
165+ "[Teamscale Java Profiler] Failed to start up and will not collect coverage: " + t );
166+ loggingFailure .addSuppressed (t );
167+ loggingFailure .printStackTrace ();
168+ } catch (Throwable ignored ) {
169+ // Nothing we can do; the alternative is to crash the profiled application, which we must not do.
170+ }
171+ }
172+ }
173+
121174 private static Pair <AgentOptions , List <Exception >> getAndApplyAgentOptions (String options ,
122175 String environmentConfigId ,
123176 String environmentConfigFile ) throws AgentOptionParseException , IOException , AgentOptionReceiveException {
@@ -204,9 +257,14 @@ private static void initializeLogging(AgentOptions agentOptions, DelayedLogger l
204257 }
205258 }
206259
207- /** Closes the opened logging contexts. */
260+ /**
261+ * Closes the opened logging contexts. Safe to call before logging has been initialized, in which case this is a
262+ * no-op.
263+ */
208264 static void closeLoggingResources () {
209- loggingResources .close ();
265+ if (loggingResources != null ) {
266+ loggingResources .close ();
267+ }
210268 }
211269
212270 /**
0 commit comments