2727import java .io .FileNotFoundException ;
2828import java .io .IOException ;
2929import java .nio .file .AtomicMoveNotSupportedException ;
30+ import java .nio .file .FileAlreadyExistsException ;
3031import java .nio .file .Files ;
3132import java .nio .file .NoSuchFileException ;
3233import java .nio .file .Path ;
3334import java .nio .file .Paths ;
3435import java .nio .file .StandardCopyOption ;
36+ import java .nio .file .StandardOpenOption ;
37+ import java .nio .file .attribute .FileTime ;
3538import java .text .DateFormat ;
3639import java .text .SimpleDateFormat ;
3740import java .util .ArrayList ;
3841import java .util .Arrays ;
42+ import java .util .ConcurrentModificationException ;
3943import java .util .Date ;
4044import java .util .List ;
4145import java .util .TimeZone ;
@@ -89,6 +93,8 @@ public final class NativeImageAgent extends JvmtiAgentBase<NativeImageAgentJNIHa
8993 private TracingResultWriter tracingResultWriter ;
9094
9195 private Path configOutputDirPath ;
96+ private Path configOutputLockFilePath ;
97+ private FileTime expectedConfigModifiedBefore ;
9298
9399 private static String getTokenValue (String token ) {
94100 return token .substring (token .indexOf ('=' ) + 1 );
@@ -244,9 +250,24 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
244250 return usage (1 , "can only once specify exactly one of trace-output=, config-output-dir= or config-merge-dir=." );
245251 }
246252 try {
247- configOutputDirPath = Paths .get (configOutputDir );
248- if (!Files .exists (configOutputDirPath )) {
249- Files .createDirectories (configOutputDirPath );
253+ configOutputDirPath = Files .createDirectories (Path .of (configOutputDir ));
254+ configOutputLockFilePath = configOutputDirPath .resolve (ConfigurationFile .LOCK_FILE_NAME );
255+ try {
256+ Files .writeString (configOutputLockFilePath , Long .toString (ProcessProperties .getProcessID ()),
257+ StandardOpenOption .CREATE_NEW , StandardOpenOption .WRITE );
258+ } catch (FileAlreadyExistsException e ) {
259+ String process ;
260+ try {
261+ process = Files .readString (configOutputLockFilePath ).stripTrailing ();
262+ } catch (Exception ignored ) {
263+ process = "(unknown)" ;
264+ }
265+ return error (2 , "Output directory '" + configOutputDirPath + "' is locked by process " + process + ", " +
266+ "which means another agent instance is already writing to this directory. " +
267+ "Only one agent instance can safely write to a specific target directory at the same time. " +
268+ "Unless file '" + ConfigurationFile .LOCK_FILE_NAME + "' is a leftover from an earlier process that terminated abruptly, it is unsafe to delete it. " +
269+ "For running multiple processes with agents at the same time to create a single configuration, read Agent.md " +
270+ "or https://www.graalvm.org/reference-manual/native-image/Agent/ on how to use the native-image-configure tool." );
250271 }
251272 if (experimentalOmitClasspathConfig ) {
252273 ignoreConfigFromClasspath (jvmti , omittedConfigs );
@@ -265,12 +286,12 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
265286 shouldExcludeClassesWithHash = omittedConfigProcessor .getPredefinedClassesConfiguration ()::containsClassWithHash ;
266287 }
267288
268- Path [] predefinedClassDestinationDirs = {configOutputDirPath .resolve (ConfigurationFile .PREDEFINED_CLASSES_AGENT_EXTRACTED_SUBDIR )};
269289 if (configurationWithOrigins ) {
270290 ConfigurationWithOriginsResultWriter writer = new ConfigurationWithOriginsResultWriter (advisor , recordKeeper );
271291 tracer = writer ;
272292 tracingResultWriter = writer ;
273293 } else {
294+ Path [] predefinedClassDestDirs = {Files .createDirectories (configOutputDirPath .resolve (ConfigurationFile .PREDEFINED_CLASSES_AGENT_EXTRACTED_SUBDIR ))};
274295 Function <IOException , Exception > handler = e -> {
275296 if (e instanceof NoSuchFileException ) {
276297 warn ("file " + ((NoSuchFileException ) e ).getFile () + " for merging could not be found, skipping" );
@@ -283,11 +304,12 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
283304 };
284305 TraceProcessor processor = new TraceProcessor (advisor , mergeConfigs .loadJniConfig (handler ), mergeConfigs .loadReflectConfig (handler ),
285306 mergeConfigs .loadProxyConfig (handler ), mergeConfigs .loadResourceConfig (handler ), mergeConfigs .loadSerializationConfig (handler ),
286- mergeConfigs .loadPredefinedClassesConfig (predefinedClassDestinationDirs , shouldExcludeClassesWithHash , handler ), omittedConfigProcessor );
307+ mergeConfigs .loadPredefinedClassesConfig (predefinedClassDestDirs , shouldExcludeClassesWithHash , handler ), omittedConfigProcessor );
287308 ConfigurationResultWriter writer = new ConfigurationResultWriter (processor );
288309 tracer = writer ;
289310 tracingResultWriter = writer ;
290311 }
312+ expectedConfigModifiedBefore = getMostRecentlyModified (configOutputDirPath , getMostRecentlyModified (configOutputLockFilePath , null ));
291313 } catch (Throwable t ) {
292314 return error (2 , t .toString ());
293315 }
@@ -342,7 +364,7 @@ private static <T> T error(T result, String message) {
342364 private static <T > T usage (T result , String message ) {
343365 inform (message );
344366 inform ("Example usage: -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/" );
345- inform ("For details, please read BuildConfiguration .md or https://www.graalvm.org/reference-manual/native-image/BuildConfiguration /" );
367+ inform ("For details, please read Agent .md or https://www.graalvm.org/reference-manual/native-image/Agent /" );
346368 return result ;
347369 }
348370
@@ -505,20 +527,39 @@ protected void onVMDeathCallback(JvmtiEnv jvmti, JNIEnvironment jni) {
505527
506528 private static final int MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES = 5 ;
507529 private static int currentFailuresWritingConfigs = 0 ;
530+ private static int currentFailuresModifiedTargetDirectory = 0 ;
508531
509532 private void writeConfigurationFiles () {
533+ Path tempDirectory = null ;
510534 try {
511- final Path tempDirectory = configOutputDirPath .toFile ().exists ()
512- ? Files .createTempDirectory (configOutputDirPath , "tempConfig-" )
513- : Files .createTempDirectory ("tempConfig-" );
514- List <Path > writtenFilePaths = tracingResultWriter .writeToDirectory (tempDirectory );
515-
516- for (Path writtenFilePath : writtenFilePaths ) {
517- Path fileName = tempDirectory .relativize (writtenFilePath );
518- Path target = configOutputDirPath .resolve (fileName );
519- tryAtomicMove (writtenFilePath , target );
535+ FileTime mostRecent = getMostRecentlyModified (configOutputDirPath , expectedConfigModifiedBefore );
536+
537+ // Write files first before failing any modification checks
538+ tempDirectory = Files .createTempDirectory (configOutputDirPath , transformPath ("agent-pid{pid}-{datetime}.tmp" ));
539+ List <Path > tempFilePaths = tracingResultWriter .writeToDirectory (tempDirectory );
540+
541+ if (!Files .exists (configOutputLockFilePath )) {
542+ throw unexpectedlyModified (configOutputLockFilePath );
543+ }
544+ expectUnmodified (configOutputLockFilePath );
545+ if (!mostRecent .equals (expectedConfigModifiedBefore )) {
546+ throw unexpectedlyModified (configOutputDirPath );
547+ }
548+
549+ Path [] targetFilePaths = new Path [tempFilePaths .size ()];
550+ for (int i = 0 ; i < tempFilePaths .size (); i ++) {
551+ Path fileName = tempDirectory .relativize (tempFilePaths .get (i ));
552+ targetFilePaths [i ] = configOutputDirPath .resolve (fileName );
553+ expectUnmodified (targetFilePaths [i ]);
520554 }
521555
556+ for (int i = 0 ; i < tempFilePaths .size (); i ++) {
557+ tryAtomicMove (tempFilePaths .get (i ), targetFilePaths [i ]);
558+ mostRecent = getMostRecentlyModified (targetFilePaths [i ], mostRecent );
559+ }
560+ mostRecent = getMostRecentlyModified (configOutputDirPath , mostRecent );
561+ expectedConfigModifiedBefore = mostRecent ;
562+
522563 /*
523564 * Note that sidecar files may be written directly to the final output directory, such
524565 * as the class files from predefined class tracking. However, such files generally
@@ -527,8 +568,39 @@ private void writeConfigurationFiles() {
527568
528569 compulsoryDelete (tempDirectory );
529570 } catch (IOException e ) {
530- warnUpToLimit (currentFailuresWritingConfigs ++, MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES , "Error when writing configuration files: " + e .toString ());
571+ warnUpToLimit (currentFailuresWritingConfigs ++, MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES , "Error when writing configuration files: " + e );
572+ } catch (ConcurrentModificationException e ) {
573+ warnUpToLimit (currentFailuresModifiedTargetDirectory ++, MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES ,
574+ "file or directory '" + e .getMessage () + "' has been modified by another process. " +
575+ "All output files remain in the temporary directory '" + configOutputDirPath .resolve (".." ).relativize (tempDirectory ) + "'. " +
576+ "Ensure that only one agent instance and no other processes are writing to the output directory '" + configOutputDirPath + "' at the same time. " +
577+ "For running multiple processes with agents at the same time to create a single configuration, read Agent.md " +
578+ "or https://www.graalvm.org/reference-manual/native-image/Agent/ on how to use the native-image-configure tool." );
579+ }
580+ }
581+
582+ private void expectUnmodified (Path path ) {
583+ try {
584+ if (Files .getLastModifiedTime (path ).compareTo (expectedConfigModifiedBefore ) > 0 ) {
585+ throw unexpectedlyModified (path );
586+ }
587+ } catch (IOException ignored ) {
588+ // best effort
589+ }
590+ }
591+
592+ private static ConcurrentModificationException unexpectedlyModified (Path path ) {
593+ throw new ConcurrentModificationException (path .getFileName ().toString ());
594+ }
595+
596+ private static FileTime getMostRecentlyModified (Path path , FileTime other ) {
597+ FileTime modified ;
598+ try {
599+ modified = Files .getLastModifiedTime (path );
600+ } catch (IOException ignored ) {
601+ return other ; // best effort
531602 }
603+ return (other == null || other .compareTo (modified ) < 0 ) ? modified : other ;
532604 }
533605
534606 private static void compulsoryDelete (Path pathToDelete ) {
@@ -559,7 +631,7 @@ private static void tryAtomicMove(final Path source, final Path target) throws I
559631 Files .move (source , target , StandardCopyOption .REPLACE_EXISTING , StandardCopyOption .ATOMIC_MOVE );
560632 } catch (AtomicMoveNotSupportedException e ) {
561633 warnUpToLimit (currentFailuresAtomicMove ++, MAX_FAILURES_ATOMIC_MOVE ,
562- String .format ("Could not move temporary configuration profile from (%s) to (%s) atomically. " +
634+ String .format ("Could not move temporary configuration profile from '%s' to '%s' atomically. " +
563635 "This might result in inconsistencies." , source .toAbsolutePath (), target .toAbsolutePath ()));
564636 Files .move (source , target , StandardCopyOption .REPLACE_EXISTING );
565637 }
@@ -585,6 +657,8 @@ protected int onUnloadCallback(JNIJavaVM vm) {
585657 if (tracingResultWriter .supportsOnUnloadTraceWriting ()) {
586658 if (configOutputDirPath != null ) {
587659 writeConfigurationFiles ();
660+ compulsoryDelete (configOutputLockFilePath );
661+ configOutputLockFilePath = null ;
588662 configOutputDirPath = null ;
589663 }
590664 }
0 commit comments