gnodet-bot commented on code in PR #29805:
URL: https://github.com/apache/camel/pull/29805#discussion_r4568973583


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java:
##########
@@ -813,33 +852,81 @@ public static JsonObject createPluginConfig(Path 
configPath) {
             if (parent != null) {
                 Files.createDirectories(parent);
             }
-            Files.writeString(f, config.toJson(),
-                    StandardOpenOption.CREATE,
-                    StandardOpenOption.WRITE,
-                    StandardOpenOption.TRUNCATE_EXISTING);
+            atomicWriteJson(f, config.toJson());
         } catch (IOException e) {
             throw new RuntimeCamelException("Failed to create plugin 
configuration", e);
         }
 
         return config;
     }
 
+    /**
+     * Writes {@code content} to {@code target} atomically: writes to a 
sibling temp file first, then moves it into
+     * place with {@link StandardCopyOption#REPLACE_EXISTING}. When atomic 
move is not supported by the underlying
+     * filesystem, falls back to a plain overwriting move (rename(2)), which 
is still safe — a reader never observes a
+     * half-written file.
+     */
+    private static void atomicWriteJson(Path target, String content) throws 
IOException {
+        // Resolve symlinks so the move replaces the target file, not the 
symlink itself.
+        Path realTarget;
+        try {
+            realTarget = target.toRealPath();
+        } catch (IOException e) {
+            // file does not exist yet — no symlink to resolve
+            realTarget = target;
+        }
+        Path dir = realTarget.getParent();
+        if (dir != null) {
+            Files.createDirectories(dir);
+        }
+        Path tmp = dir != null
+                ? Files.createTempFile(dir, "." + realTarget.getFileName(), 
".tmp")
+                : Files.createTempFile("." + realTarget.getFileName(), ".tmp");
+        try {
+            Files.writeString(tmp, content, StandardOpenOption.WRITE, 
StandardOpenOption.TRUNCATE_EXISTING);
+            try {
+                Files.move(tmp, realTarget, 
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+                tmp = null; // moved successfully, nothing to delete
+            } catch (AtomicMoveNotSupportedException e) {
+                // fall back: non-atomic replace — still better than 
truncate-then-write
+                Files.move(tmp, realTarget, 
StandardCopyOption.REPLACE_EXISTING);
+                tmp = null;
+            }
+        } finally {
+            if (tmp != null) {
+                try {
+                    Files.deleteIfExists(tmp);
+                } catch (IOException ignored) {
+                    // best effort
+                }
+            }
+        }
+    }
+
     public static void savePluginConfig(JsonObject plugins) {
         savePluginConfig(plugins, null);
     }
 
     public static void savePluginConfig(JsonObject plugins, Path configPath) {
         Path f = getCustomPluginConfigPath(configPath);
         try {
-            Files.writeString(f, plugins.toJson(),
-                    StandardOpenOption.CREATE,
-                    StandardOpenOption.WRITE,
-                    StandardOpenOption.TRUNCATE_EXISTING);
+            atomicWriteJson(f, plugins.toJson());
         } catch (IOException e) {
             throw new RuntimeCamelException("Failed to save plugin 
configuration", e);
         }
     }
 
+    /**
+     * Warns about a corrupt or unreadable plugin configuration file, once per 
path per JVM run, so multiple callers
+     * (e.g. {@code getActivePlugins} and {@code printAvailablePlugins}) do 
not repeat the message.
+     */
+    public static void warnCorruptPluginConfig(Printer printer, Exception e) {
+        String key = e.getMessage() != null ? e.getMessage() : 
"unknown-corrupt-plugin-config";

Review Comment:
   💡 **Minor:** `WARNED_PLUGIN_PATHS` uses `e.getMessage()` as the dedup key, 
while the sibling `CommandLineHelper.WARNED_PATHS` uses the file's absolute 
path. The message works here because `getPluginConfig()` embeds the file path 
in the `RuntimeCamelException` — but it's fragile (varying parse-error details 
for the same file could produce different keys). Consider passing the path 
explicitly and using it as the key for consistency:
   
   ```suggestion
       public static void warnCorruptPluginConfig(Printer printer, String 
configPath, Exception e) {
           if (WARNED_PLUGIN_PATHS.add(configPath)) {
               printer.printErr("Warning: " + e.getMessage() + ". Fix the JSON 
by hand or delete the file.");
           }
       }
   ```
   (callers would pass 
`getCustomPluginConfigPath(configPath).toAbsolutePath().toString()`)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to