Skip to content

Commit 2491c2c

Browse files
ArloLjormundur00
andauthored
Fix order of access filter files by moving default to the start (#875)
* Return empty command line for disabled AgentConfiguration * Refactor addDefaultAccessFilter into pure getDefaultAccessFilter * Inject agent-config directory into AgentConfiguration * Don't swallow FileAlreadyExistsException in getDefaultAccessFilter FileAlreadyExistsException from Files.createDirectories means the path exists but is not a directory — that should surface, not be logged and ignored. Files.createDirectories already no-ops when the directory exists, so the catch was masking a real error case. * Simplify access-filter write to a direct copy The temp-file-plus-atomic-move pattern was guarding against concurrent writers racing on the same accessFilterFile, but getDefaultAccessFilter is not called concurrently for the same agentConfigDir. Copy straight to the destination instead. * Remove unused logger * Store agent-config directory as a String for serializability AgentConfiguration implements Serializable and is carried across the Gradle configuration-cache boundary through serializable provider transformations. java.nio.file.Path is not Serializable (UnixPath throws NotSerializableException), so storing the agent-config directory as a Path could break the configuration-cache path. Store it as a String and reconstruct the Path via Path.of(...) when building the default access filter. Callers convert at the constructor boundary. Adds a regression test asserting the configuration serializes. * Specify access-filter precedence in the functional spec Document the built-in-default-before-user-filters ordering and the disabled-configuration behavior as §FS-common-libraries.3.1.1, and cite it at the implementation and tests. * Fix access filter lifecycle handling * Clean up Maven agent filter directory --------- Co-authored-by: jvukicev <jovan.vukicevic@oracle.com>
1 parent dd2072b commit 2491c2c

13 files changed

Lines changed: 383 additions & 54 deletions

File tree

common/docs/functional-spec.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ option list. Advanced options such as caller filters, access filters, predefined
5656
allocation tracing, and reflection metadata tracking must be represented once so both plugins
5757
expose the same mode semantics.
5858

59+
#### 3.1.1 Access-filter precedence
60+
61+
The shared agent model must emit its built-in default access filter before every user-provided
62+
access-filter file, including filters supplied as raw direct-mode options, so that user
63+
configuration takes precedence over the built-in defaults.
64+
`native-image-agent` evaluates access filters in order and lets later rules override earlier ones,
65+
so ordering the built-in filter first leaves it as a baseline that user rules override.
66+
67+
A disabled agent configuration must emit no agent command-line options.
68+
69+
Constructing the agent command line must not create the built-in filter. The shared model describes
70+
the filter's path, while each build-tool adapter must materialize the file at a lifecycle-safe point.
71+
5972
### 3.2 Plugin invocation and output
6073

6174
Plugin-specific enablement, instrumentation hooks, and output locations are specified by

common/utils/src/main/java/org/graalvm/buildtools/agent/AgentConfiguration.java

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -43,43 +43,45 @@
4343
import java.io.IOException;
4444
import java.io.InputStream;
4545
import java.io.Serializable;
46-
import java.nio.file.FileAlreadyExistsException;
4746
import java.nio.file.Files;
4847
import java.nio.file.Path;
4948
import java.nio.file.StandardCopyOption;
5049
import java.util.ArrayList;
5150
import java.util.Collection;
5251
import java.util.List;
53-
import java.util.logging.Logger;
5452

5553
public class AgentConfiguration implements Serializable {
5654

5755
private static final String ACCESS_FILTER_PREFIX = "access-filter";
5856
private static final String ACCESS_FILTER_SUFFIX = ".json";
5957
private static final String DEFAULT_ACCESS_FILTER_FILE_LOCATION = "/" + ACCESS_FILTER_PREFIX + ACCESS_FILTER_SUFFIX;
6058

61-
private static final Logger logger = Logger.getGlobal();
62-
6359
private final Collection<String> callerFilterFiles;
6460
private final Collection<String> accessFilterFiles;
6561
private final Boolean builtinCallerFilter;
6662
private final Boolean builtinHeuristicFilter;
6763
private final Boolean experimentalPredefinedClasses;
6864
private final Boolean experimentalUnsafeAllocationTracing;
6965
private final Boolean trackReflectionMetadata;
66+
private final String defaultAccessFilterFile;
7067

7168
private final AgentMode agentMode;
7269

7370
// This constructor should be used only to specify that we have instance of agent that is disabled (to avoid using null for agent enable check)
74-
public AgentConfiguration(AgentMode... modes) {
71+
public AgentConfiguration() {
72+
this(null, new DisabledAgentMode());
73+
}
74+
75+
public AgentConfiguration(String defaultAccessFilterFile, AgentMode agentMode) {
7576
this.callerFilterFiles = null;
7677
this.accessFilterFiles = null;
7778
this.builtinCallerFilter = null;
7879
this.builtinHeuristicFilter = null;
7980
this.experimentalPredefinedClasses = null;
8081
this.experimentalUnsafeAllocationTracing = null;
8182
this.trackReflectionMetadata = null;
82-
this.agentMode = modes.length == 1 ? modes[0] : new DisabledAgentMode();
83+
this.defaultAccessFilterFile = defaultAccessFilterFile;
84+
this.agentMode = agentMode;
8385
}
8486

8587
public AgentConfiguration(Collection<String> callerFilterFiles,
@@ -89,7 +91,8 @@ public AgentConfiguration(Collection<String> callerFilterFiles,
8991
Boolean experimentalPredefinedClasses,
9092
Boolean experimentalUnsafeAllocationTracing,
9193
Boolean trackReflectionMetadata,
92-
AgentMode agentMode) {
94+
AgentMode agentMode,
95+
String defaultAccessFilterFile) {
9396
this.callerFilterFiles = callerFilterFiles;
9497
this.accessFilterFiles = accessFilterFiles;
9598
this.builtinCallerFilter = builtinCallerFilter;
@@ -98,11 +101,18 @@ public AgentConfiguration(Collection<String> callerFilterFiles,
98101
this.experimentalUnsafeAllocationTracing = experimentalUnsafeAllocationTracing;
99102
this.trackReflectionMetadata = trackReflectionMetadata;
100103
this.agentMode = agentMode;
104+
this.defaultAccessFilterFile = defaultAccessFilterFile;
101105
}
102106

103107
public List<String> getAgentCommandLine() {
104-
addDefaultAccessFilter();
105-
List<String> cmdLine = new ArrayList<>(agentMode.getAgentCommandLine());
108+
if (!isEnabled()) {
109+
return List.of();
110+
}
111+
List<String> cmdLine = new ArrayList<>();
112+
// The default filter precedes user filters so user rules have final precedence.
113+
// §FS-common-libraries.3.1.1
114+
cmdLine.add("access-filter-file=" + defaultAccessFilterFile);
115+
cmdLine.addAll(agentMode.getAgentCommandLine());
106116
appendOptionToValues("caller-filter-file=", callerFilterFiles, cmdLine);
107117
appendOptionToValues("access-filter-file=", accessFilterFiles, cmdLine);
108118
addToCmd("builtin-caller-filter=", builtinCallerFilter, cmdLine);
@@ -114,7 +124,8 @@ public List<String> getAgentCommandLine() {
114124
}
115125

116126
public Collection<String> getAgentFiles() {
117-
List<String> files = new ArrayList<>(callerFilterFiles.size() + accessFilterFiles.size());
127+
List<String> files = new ArrayList<>(callerFilterFiles.size() + accessFilterFiles.size() + 1);
128+
files.add(defaultAccessFilterFile);
118129
files.addAll(callerFilterFiles);
119130
files.addAll(accessFilterFiles);
120131
files.addAll(agentMode.getInputFiles());
@@ -141,42 +152,17 @@ private void addToCmd(String option, Boolean value, List<String> cmdLine) {
141152
}
142153
}
143154

144-
private void addDefaultAccessFilter() {
145-
if (accessFilterFiles == null) {
146-
// this could only happen if we instantiated disabled agent configuration
147-
return;
148-
}
149-
150-
String tempDir = System.getProperty("java.io.tmpdir");
151-
Path agentDir = Path.of(tempDir).resolve("agent-config");
152-
Path accessFilterFile = agentDir.resolve(ACCESS_FILTER_PREFIX + ACCESS_FILTER_SUFFIX);
153-
if (Files.exists(accessFilterFile)) {
154-
accessFilterFiles.add(accessFilterFile.toString());
155-
return;
156-
}
155+
public static Path getDefaultAccessFilterPath(Path agentConfigDir) {
156+
return agentConfigDir.resolve(ACCESS_FILTER_PREFIX + ACCESS_FILTER_SUFFIX);
157+
}
157158

159+
public static void writeDefaultAccessFilter(Path accessFilterFile) {
158160
try (InputStream accessFilterData = AgentConfiguration.class.getResourceAsStream(DEFAULT_ACCESS_FILTER_FILE_LOCATION)) {
159161
if (accessFilterData == null) {
160162
throw new IOException("Cannot access data from: " + DEFAULT_ACCESS_FILTER_FILE_LOCATION);
161163
}
162-
163-
try {
164-
Files.createDirectory(agentDir);
165-
} catch (FileAlreadyExistsException e) {
166-
logger.info("Skip creation of directory " + agentDir + " (already created).");
167-
}
168-
169-
Path tmpAccessFilter = Files.createTempFile(agentDir, ACCESS_FILTER_PREFIX, ACCESS_FILTER_SUFFIX);
170-
Files.copy(accessFilterData, tmpAccessFilter, StandardCopyOption.REPLACE_EXISTING);
171-
172-
try {
173-
Files.move(tmpAccessFilter, accessFilterFile, StandardCopyOption.ATOMIC_MOVE);
174-
} catch (FileAlreadyExistsException e) {
175-
Files.delete(tmpAccessFilter);
176-
logger.info(accessFilterFile + " already exists. Delete " + tmpAccessFilter);
177-
}
178-
179-
accessFilterFiles.add(accessFilterFile.toString());
164+
Files.createDirectories(accessFilterFile.getParent());
165+
Files.copy(accessFilterData, accessFilterFile, StandardCopyOption.REPLACE_EXISTING);
180166
} catch (IOException e) {
181167
throw new RuntimeException("Cannot add default access-filter.json", e);
182168
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package org.graalvm.buildtools.agent;
42+
43+
import org.junit.jupiter.api.Test;
44+
import org.junit.jupiter.api.io.TempDir;
45+
46+
import java.io.ByteArrayOutputStream;
47+
import java.io.ObjectOutputStream;
48+
import java.nio.file.Files;
49+
import java.nio.file.Path;
50+
import java.util.ArrayList;
51+
import java.util.List;
52+
53+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
54+
import static org.junit.jupiter.api.Assertions.assertEquals;
55+
import static org.junit.jupiter.api.Assertions.assertFalse;
56+
import static org.junit.jupiter.api.Assertions.assertTrue;
57+
58+
class AgentConfigurationTest {
59+
60+
private static final String ACCESS_FILTER_OPTION = "access-filter-file=";
61+
62+
@TempDir
63+
Path agentConfigDir;
64+
65+
// §FS-common-libraries.3.1.1
66+
@Test
67+
void disabledConfigurationReturnsEmptyCommandLine() {
68+
AgentConfiguration disabled = new AgentConfiguration();
69+
70+
List<String> cmdLine = disabled.getAgentCommandLine();
71+
72+
assertTrue(cmdLine.isEmpty(), "Disabled agent configuration should not emit any options, but was: " + cmdLine);
73+
}
74+
75+
// §FS-common-libraries.3.1.1
76+
@Test
77+
void standardConfigurationEmitsDefaultAccessFilterBeforeUserAccessFilters() {
78+
List<String> userAccessFilters = List.of("user-1.json", "user-2.json");
79+
Path defaultAccessFilter = AgentConfiguration.getDefaultAccessFilterPath(agentConfigDir);
80+
AgentConfiguration configuration = new AgentConfiguration(
81+
List.of(),
82+
new ArrayList<>(userAccessFilters),
83+
null,
84+
null,
85+
null,
86+
null,
87+
null,
88+
new StandardAgentMode(),
89+
defaultAccessFilter.toString());
90+
91+
List<String> cmdLine = configuration.getAgentCommandLine();
92+
93+
List<String> accessFilterOptions = cmdLine.stream()
94+
.filter(option -> option.startsWith(ACCESS_FILTER_OPTION))
95+
.toList();
96+
97+
assertEquals(userAccessFilters.size() + 1, accessFilterOptions.size(),
98+
"Expected one default access filter plus user access filters, but got: " + accessFilterOptions);
99+
assertEquals(ACCESS_FILTER_OPTION + defaultAccessFilter, accessFilterOptions.get(0));
100+
assertFalse(Files.exists(defaultAccessFilter), "Constructing the command line must not create the default filter");
101+
102+
for (int i = 0; i < userAccessFilters.size(); i++) {
103+
assertEquals(ACCESS_FILTER_OPTION + userAccessFilters.get(i), accessFilterOptions.get(i + 1),
104+
"User access filters must follow the default in their original order");
105+
}
106+
}
107+
108+
// Raw direct-mode options must follow the built-in default without being reordered. §FS-common-libraries.3.1.1
109+
@Test
110+
void directConfigurationEmitsDefaultAccessFilterBeforeModeOptions() {
111+
List<String> directOptions = List.of(
112+
"config-output-dir=metadata",
113+
"access-filter-file=direct-1.json",
114+
"access-filter-file=direct-2.json");
115+
Path defaultAccessFilter = AgentConfiguration.getDefaultAccessFilterPath(agentConfigDir);
116+
AgentConfiguration configuration = new AgentConfiguration(
117+
List.of(),
118+
List.of(),
119+
null,
120+
null,
121+
null,
122+
null,
123+
null,
124+
new DirectAgentMode(directOptions),
125+
defaultAccessFilter.toString());
126+
127+
List<String> cmdLine = configuration.getAgentCommandLine();
128+
129+
assertEquals(ACCESS_FILTER_OPTION + defaultAccessFilter, cmdLine.get(0));
130+
assertEquals(directOptions, cmdLine.subList(1, directOptions.size() + 1));
131+
assertFalse(Files.exists(defaultAccessFilter), "Constructing the command line must not create the default filter");
132+
}
133+
134+
@Test
135+
void configurationIsSerializable() {
136+
Path defaultAccessFilter = AgentConfiguration.getDefaultAccessFilterPath(agentConfigDir);
137+
AgentConfiguration configuration = new AgentConfiguration(
138+
List.of(),
139+
new ArrayList<>(List.of("user-1.json")),
140+
null,
141+
null,
142+
null,
143+
null,
144+
null,
145+
new StandardAgentMode(),
146+
defaultAccessFilter.toString());
147+
148+
assertDoesNotThrow(() -> {
149+
try (ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream())) {
150+
out.writeObject(configuration);
151+
}
152+
}, "AgentConfiguration must be serializable to support the Gradle configuration cache");
153+
}
154+
}

native-gradle-plugin/docs/functional/tracing-agent.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ Gradle-managed agent output directory so users can find collected metadata witho
4141
aligning with [§root/GOAL-concise-actionable-output](../../../docs/spec/goals.md#goal-concise-actionable-output-build-output-is-concise-actionable-and-token-efficient). Generated output must be suitable for later
4242
merge and copy steps.
4343

44+
### 4.1 Default access filter
45+
46+
When the agent is enabled, Gradle must generate the built-in default access filter as a declared
47+
task output under `build/native/agent-config` before an instrumented task runs. Configuration and
48+
command-line argument calculation must remain side-effect-free so generating the filter does not
49+
invalidate the configuration cache.
50+
4451
## 5. Metadata copy
4552

4653
`metadataCopy` copies or merges agent output from configured input tasks into configured output

native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/JavaApplicationWithAgentFunctionalTest.groovy

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ org.gradle.java.installations.auto-download=false
427427
junitVersion = System.getProperty('versions.junit')
428428
}
429429

430+
// The default filter is generated as task output without invalidating the configuration cache. §FS-tracing-agent.4.1
430431
@Unroll("plugin supports configuration cache (JUnit Platform #junitVersion)")
431432
def "supports configuration cache"() {
432433
given:
@@ -437,12 +438,14 @@ org.gradle.java.installations.auto-download=false
437438

438439
then:
439440
tasks {
440-
succeeded ':run'
441+
succeeded ':generateAgentAccessFilter',
442+
':run'
441443
doesNotContain ':jar'
442444
}
443445

444446
and:
445447
assert metadataExistsAt("build/native/agent-output/run")
448+
file("build/native/agent-config/access-filter.json").exists()
446449

447450
when:
448451
run'run', '-Pagent', '--configuration-cache', '--rerun-tasks'

0 commit comments

Comments
 (0)