oscerd commented on code in PR #27399:
URL: https://github.com/apache/camel/pull/27399#discussion_r4193672692


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java:
##########
@@ -218,6 +238,61 @@ private static RuntimeHelper.ProcessInfo findByPid(long 
pid) {
         return null;
     }
 
+    /**
+     * The console output of a run that failed to start, as camel_get_log 
shows a log (CAMEL-25364): each record keeps
+     * its message, an error its root cause and the first line of the user's 
code, and the stack frames are left out.
+     * The end of the raw output is mostly frames of the runtime, and the line 
that says what went wrong is above them.
+     */
+    static String failureOutput(Path output) {
+        List<String> lines;
+        try {
+            lines = Files.readAllLines(output, StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            return readTail(output);
+        }
+        if (lines.size() > 5000) {
+            lines = lines.subList(lines.size() - 5000, lines.size());
+        }
+        lines = lines.stream().filter(l -> !l.isBlank()).toList();
+        JsonObject log = LogFileReader.build(lines, FAILURE_RECORDS, null, 
null, false, new JsonObject());

Review Comment:
   **Multi-line exception messages (e.g. javac diagnostics) are dropped with 
the stack trace**
   
   `failureOutput` folds with `details=false`, and `LogFileReader.withoutTrace` 
then removes the whole `detail` of every record whose continuation lines 
contain a frame, keeping the head line, `cause` (the first line of the root 
cause, only when the head does not already contain it) and `at`. When the 
exception message itself spans several lines, the cause is lost. That includes 
the case this issue is about, an added Java class that does not compile: 
`MultiCompile` throws `new ReflectException("Compilation error:\n" + sj + "\n" 
+ out)`, and `KameletMain` logs it as `LOG.error("Error starting Camel: {}", e, 
e)`. The head line ends at `Compilation error:`, and the javac diagnostics 
(`OrderNumber.java:12: error: cannot find symbol`, ...) are continuation lines 
in the same block as the `\tat` frames, so the result is only:
   
   ```
   ERROR Error starting Camel: org.joor.ReflectException: Compilation error:
     (N lines of stack trace left out)
   ```
   
   Unlike `camel_get_log` (which has `details=true`), here the folded text is 
the only copy the agent gets: `Run` deletes `~/.camel/<pid>.log` on exit, and 
the path of the capture is not returned.
   
   Relatedly, `readTail` only runs when the output is blank, so non-empty 
output without log records becomes one record headed by its first line, capped 
at 20 detail lines; the end, where a launcher prints its error, is cut, which 
does not match "Output with no log records is returned as it is".
   
   Keeping the non-frame continuation lines (e.g. drop only the frame and `... 
N more` lines before folding, or keep each exception's lines up to its first 
frame), falling back to `readTail` when no line matched the log pattern, and a 
test fixture with a multi-line exception message would cover it.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java:
##########
@@ -72,6 +76,22 @@ public static JsonObject run(Path directory, List<String> 
files, String name, bo
             return result;
         }
         result.put("directory", directory.toString());
+        if (sourceDir) {
+            // the whole directory is the app: one already running from it is 
that app, and a second copy only fights
+            // it for ports and files and makes its name ambiguous to the 
other tools
+            RuntimeHelper.ProcessInfo running = runningFrom(directory);
+            if (running != null) {
+                String runningName = running.contextName() != null && 
!running.contextName().isBlank()
+                        ? running.contextName() : running.name();
+                result.put("status", "running");
+                result.put("pid", running.pid());
+                result.put("name", runningName);
+                result.put("message", runningName + " (pid " + running.pid() + 
") already runs from this directory;"
+                                      + " a second copy was not started. In 
dev mode a changed or added file is reloaded;"
+                                      + " camel_control restart restarts it, 
for example after adding a Java class.");

Review Comment:
   **The reload/restart advice does not hold for an app started with explicit 
files**
   
   The guard returns any process whose working directory is this directory, 
which may be one started earlier with explicit files (by `camel_run` with 
`files`, or a user's `camel run a.camel.yaml`) or without `--dev`. For an 
explicit-files run, dev mode only watches the listed files and `camel_control 
restart` replays the same command line, so neither picks up an added Java 
class, yet the message promises both. `restart()` already reads 
`ProcessHandle.info().arguments()`, so the guard could treat the process as the 
app only when it was started with `--source-dir`, report dev mode from `--dev`, 
and otherwise say that an integration started with explicit files runs there.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java:
##########
@@ -72,6 +76,22 @@ public static JsonObject run(Path directory, List<String> 
files, String name, bo
             return result;
         }
         result.put("directory", directory.toString());
+        if (sourceDir) {
+            // the whole directory is the app: one already running from it is 
that app, and a second copy only fights
+            // it for ports and files and makes its name ambiguous to the 
other tools
+            RuntimeHelper.ProcessInfo running = runningFrom(directory);

Review Comment:
   **A copy that is still starting, or launched by a concurrent call, is 
invisible to the guard**
   
   `runningFrom` only sees processes that have already written 
`<pid>-status.json`. So after `run()` returns `status: starting` at the startup 
timeout (typical on a first run, while dependencies download), a second 
`camel_run` starts a second copy, and so do two calls in flight at the same 
time. Remembering what this launcher started (e.g. a map from the directory to 
the `ProcessHandle`, checked with `isAlive()`) alongside the status-file lookup 
would cover that window.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java:
##########
@@ -218,6 +238,61 @@ private static RuntimeHelper.ProcessInfo findByPid(long 
pid) {
         return null;
     }
 
+    /**
+     * The console output of a run that failed to start, as camel_get_log 
shows a log (CAMEL-25364): each record keeps
+     * its message, an error its root cause and the first line of the user's 
code, and the stack frames are left out.
+     * The end of the raw output is mostly frames of the runtime, and the line 
that says what went wrong is above them.
+     */
+    static String failureOutput(Path output) {
+        List<String> lines;
+        try {
+            lines = Files.readAllLines(output, StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            return readTail(output);
+        }
+        if (lines.size() > 5000) {
+            lines = lines.subList(lines.size() - 5000, lines.size());
+        }
+        lines = lines.stream().filter(l -> !l.isBlank()).toList();
+        JsonObject log = LogFileReader.build(lines, FAILURE_RECORDS, null, 
null, false, new JsonObject());
+        JsonArray rows = (JsonArray) log.get("lines");
+        if (rows == null || rows.isEmpty()) {
+            return readTail(output);
+        }
+        StringBuilder sb = new StringBuilder();
+        // newest first in the log tool; oldest first here, so it reads as the 
console did
+        for (int i = rows.size() - 1; i >= 0; i--) {
+            JsonObject r = (JsonObject) rows.get(i);
+            String level = r.getStringOrDefault("time", "").isEmpty() ? "" : 
r.getStringOrDefault("level", "") + " ";
+            sb.append(level).append(r.getStringOrDefault("message", ""));
+            if (r.get("cause") != null) {
+                sb.append("\n  cause: ").append(r.getString("cause"));
+            }
+            if (r.get("at") != null) {
+                sb.append("\n  at: ").append(r.getString("at"));
+            }
+            if (r.get("detailLines") != null) {
+                sb.append("\n  (").append(r.get("detailLines")).append(" lines 
of stack trace left out)");
+            } else if (r.get("detail") != null) {
+                sb.append("\n").append(r.getString("detail"));
+            }
+            sb.append("\n");
+        }
+        return sb.toString().strip();
+    }
+
+    /** The integration that runs with this directory as its working 
directory, or null. */
+    private static RuntimeHelper.ProcessInfo runningFrom(Path directory) {
+        Path wanted = directory.toAbsolutePath().normalize();
+        for (RuntimeHelper.ProcessInfo p : RuntimeHelper.discoverProcesses()) {
+            Path dir = workingDirectory(p.pid());
+            if (dir != null && 
dir.toAbsolutePath().normalize().equals(wanted)) {

Review Comment:
   **The guard misses the running app when the directory path goes through a 
symlink**
   
   `runningFrom` compares `toAbsolutePath().normalize()` of two paths from 
different sources: `runtime.directory` is the child's `new 
File(".").getAbsolutePath()`, i.e. its symlink-resolved working directory, 
while the caller's directory only goes through `toAbsolutePath().normalize()`. 
When the agent passes a path through a symlink (every project under `/tmp` or 
`$TMPDIR` on macOS), the guard never matches and a second copy is started. Both 
paths are existing directories here, so `Files.isSameFile(dir, wanted)` 
(falling back to the current comparison on `IOException`) or comparing 
`toRealPath()` would fix it.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java:
##########
@@ -218,6 +238,61 @@ private static RuntimeHelper.ProcessInfo findByPid(long 
pid) {
         return null;
     }
 
+    /**
+     * The console output of a run that failed to start, as camel_get_log 
shows a log (CAMEL-25364): each record keeps
+     * its message, an error its root cause and the first line of the user's 
code, and the stack frames are left out.
+     * The end of the raw output is mostly frames of the runtime, and the line 
that says what went wrong is above them.
+     */
+    static String failureOutput(Path output) {
+        List<String> lines;
+        try {
+            lines = Files.readAllLines(output, StandardCharsets.UTF_8);

Review Comment:
   **Strict UTF-8 decoding: a single non-UTF-8 byte makes the failure output 
empty**
   
   `Files.readAllLines(output, UTF_8)` throws `MalformedInputException` on any 
malformed byte, and `readTail`'s `Files.readString(output, UTF_8)` throws the 
same, so the output comes back empty. Console output is not always UTF-8 (a 
redirected stdout on Windows uses the native code page, so `C:\Users\Jörg\...` 
is enough). `LogFileReader.tail` already decodes leniently; doing the same here 
(and in `readTail`) keeps the output:
   
   ```suggestion
               lines = new String(Files.readAllBytes(output), 
StandardCharsets.UTF_8).lines().toList();
   ```



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java:
##########
@@ -72,6 +76,22 @@ public static JsonObject run(Path directory, List<String> 
files, String name, bo
             return result;
         }
         result.put("directory", directory.toString());
+        if (sourceDir) {
+            // the whole directory is the app: one already running from it is 
that app, and a second copy only fights
+            // it for ports and files and makes its name ambiguous to the 
other tools
+            RuntimeHelper.ProcessInfo running = runningFrom(directory);
+            if (running != null) {
+                String runningName = running.contextName() != null && 
!running.contextName().isBlank()
+                        ? running.contextName() : running.name();
+                result.put("status", "running");
+                result.put("pid", running.pid());
+                result.put("name", runningName);

Review Comment:
   **The 'running' result has no log file**
   
   Both `camel_run` descriptions promise the log file ("Returns the pid and log 
file"), and the `started` result carries `log`, but the new `running` result 
does not:
   
   ```suggestion
                   result.put("name", runningName);
                   result.put("log", LogFileReader.logFile(running.pid(), 
running.name()).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