davsclaus commented on code in PR #28107:
URL: https://github.com/apache/camel/pull/28107#discussion_r4313267617


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java:
##########
@@ -0,0 +1,423 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * The log of the previous run of an integration, as <tt>kubectl logs 
--previous</tt> shows the log of the previous
+ * instance of a container in Kubernetes.
+ * <p/>
+ * When <tt>camel run</tt> ends (it was stopped, it failed to start, or it 
stopped by itself), its log is kept as
+ * <tt>~/.camel/&lt;name&gt;-previous.log</tt>, by the name of the 
integration, with
+ * <tt>~/.camel/&lt;name&gt;-previous.json</tt>, which says when and how the 
run ended. The next run of that name that
+ * ends replaces both. Like Kubernetes, at most the last 10 MB of a log are 
kept.
+ * <p/>
+ * A process that is gone without keeping its log (it was killed, or its JVM 
crashed) leaves its
+ * <tt>~/.camel/&lt;pid&gt;.log</tt> behind. That log is kept for it, under 
the name in its status file, the next time
+ * the previous runs are looked up, and the next time an integration is run.
+ */
+public final class PreviousRun {
+
+    /**
+     * The end of a log that is kept, at most: Kubernetes keeps at most 10 MB 
of the log of a container by default.
+     */
+    static final long MAX_SIZE = 10L * 1024 * 1024;
+
+    /**
+     * How old the log of a process that is gone may get, when there is no 
name to keep it by, before it is removed.
+     */
+    static final long ORPHAN_AGE = TimeUnit.DAYS.toMillis(1);
+
+    /**
+     * What a process that is gone without keeping its log ended with, as far 
as is known.
+     */
+    static final String GONE = "the process was gone without stopping Camel 
(killed, or its JVM crashed)";
+
+    private static final String LOG_SUFFIX = "-previous.log";
+    private static final String INFO_SUFFIX = "-previous.json";
+    private static final Pattern PID_LOG = Pattern.compile("(\\d+)\\.log");
+    private static final int MAX_FAILURE = 500;
+    private static final DateTimeFormatter TIME
+            = DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm:ss").withZone(ZoneId.systemDefault());
+
+    /**
+     * A run that ended, and the log it kept.
+     *
+     * @param name      the name of the integration
+     * @param pid       the pid it ran as, 0 when not known
+     * @param directory the directory it ran in, or <tt>null</tt>
+     * @param started   when it started, in epoch millis, 0 when not known
+     * @param ended     when it ended, in epoch millis
+     * @param exitCode  the exit code, or <tt>null</tt> when not known (such 
as of a process that was killed)
+     * @param failure   why it ended, when it failed (such as the error it 
failed to start with), or <tt>null</tt>
+     * @param log       the log it kept
+     */
+    public record Info(String name, long pid, String directory, long started, 
long ended, Integer exitCode,
+            String failure, Path log) {
+
+        /**
+         * Which run it was, and how it ended, in one line.
+         */
+        public String describe() {
+            return "The previous run of " + name + (pid > 0 ? " (pid " + pid + 
")" : "") + " " + howItEnded();
+        }
+
+        /**
+         * How the run ended: when, with which exit code, and why when it 
failed.
+         */
+        public String howItEnded() {
+            StringBuilder sb = new StringBuilder("ended at 
").append(TIME.format(Instant.ofEpochMilli(ended)));
+            long ago = System.currentTimeMillis() - ended;
+            if (ago >= 0) {
+                sb.append(" (").append(TimeUtils.printDuration(ago, 
false)).append(" ago)");
+            }
+            if (exitCode != null) {
+                sb.append(" with exit code ").append(exitCode);
+            }
+            if (failure != null) {
+                sb.append(": ").append(failure);
+            }
+            return sb.toString();
+        }
+    }
+
+    private PreviousRun() {
+    }
+
+    /**
+     * Keeps the log of a run of this JVM that ended as the previous log of 
its name.
+     *
+     * @param  log      the log of the run
+     * @param  name     the name of the integration
+     * @param  pid      the pid it ran as, 0 when not known
+     * @param  exitCode the exit code, or <tt>null</tt> when not known
+     * @param  failure  why it ended, when it failed, or <tt>null</tt>
+     * @return          the kept log, or <tt>null</tt> when there was no log 
to keep
+     */
+    public static Path keep(Path log, String name, long pid, Integer exitCode, 
String failure) {
+        long started = 
ProcessHandle.current().info().startInstant().map(Instant::toEpochMilli).orElse(0L);
+        return keep(log, new Info(
+                name, pid, System.getProperty("user.dir"), started, 
System.currentTimeMillis(),
+                exitCode, failure, null));
+    }
+
+    /**
+     * Keeps the log of a run that ended as the previous log of its name, in 
place of the one kept before.
+     *
+     * @return the kept log, or <tt>null</tt> when there was no log to keep
+     */
+    static Path keep(Path log, Info info) {
+        if (log == null || info.name() == null || info.name().isBlank() || 
!Files.isRegularFile(log)) {
+            return null;
+        }
+        Path dir = CommandLineHelper.getCamelDir();
+        String file = fileName(info.name());
+        Path target = dir.resolve(file + LOG_SUFFIX);
+        try {
+            if (Files.size(log) > MAX_SIZE || !move(log, target)) {
+                copyEnd(log, target, MAX_SIZE);
+                try {
+                    Files.deleteIfExists(log);
+                } catch (IOException e) {
+                    // still open (Windows): removed when the process is gone 
(see keepOrphans)
+                }
+            }
+            JsonObject jo = new JsonObject();
+            jo.put("name", info.name());
+            if (info.pid() > 0) {
+                jo.put("pid", info.pid());
+            }
+            if (info.directory() != null) {
+                jo.put("directory", info.directory());
+            }
+            if (info.started() > 0) {
+                jo.put("started", info.started());
+            }
+            jo.put("ended", info.ended());
+            if (info.exitCode() != null) {
+                jo.put("exitCode", info.exitCode());
+            }
+            if (info.failure() != null) {
+                String failure = info.failure().strip();
+                jo.put("failure", failure.length() > MAX_FAILURE ? 
failure.substring(0, MAX_FAILURE) + "..." : failure);
+            }
+            Files.writeString(dir.resolve(file + INFO_SUFFIX), jo.toJson());
+            return target;
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    /**
+     * The previous run of each name, the newest first.
+     */
+    public static List<Info> findAll() {
+        keepOrphans();
+        List<Info> answer = new ArrayList<>();
+        File[] files = CommandLineHelper.getCamelDir().toFile().listFiles((d, 
n) -> n.endsWith(INFO_SUFFIX));
+        if (files != null) {
+            for (File f : files) {
+                Info info = read(f.toPath());
+                if (info != null) {
+                    answer.add(info);
+                }
+            }
+        }
+        answer.sort(Comparator.comparingLong(Info::ended).reversed());
+        return answer;
+    }
+
+    /**
+     * The previous runs that match the name or pid, as a running integration 
is found (see {@link ProcessFinder}): a
+     * number is the pid it ran as, and a name is a prefix (<tt>foo</tt> 
matches <tt>foo-bar</tt>) unless it ends with
+     * <tt>!</tt>, and may use wildcards. A name that is the whole name of a 
previous run matches only that run.
+     *
+     * @param nameOrPid a name, a pattern or a pid; <tt>null</tt> or 
<tt>*</tt> for every previous run
+     */
+    public static List<Info> find(String nameOrPid) {
+        List<Info> all = findAll();
+        if (nameOrPid == null || nameOrPid.isBlank() || "*".equals(nameOrPid)) 
{
+            return all;
+        }
+        if (nameOrPid.matches("\\d+")) {
+            long pid = Long.parseLong(nameOrPid);
+            return all.stream().filter(i -> i.pid() == pid).toList();
+        }
+        boolean exact = nameOrPid.endsWith("!");
+        String name = exact ? nameOrPid.substring(0, nameOrPid.length() - 1) : 
nameOrPid;
+        List<Info> same = all.stream().filter(i -> 
i.name().equals(name)).toList();
+        if (exact || !same.isEmpty()) {
+            return same;
+        }
+        String pattern = name.endsWith("*") ? name : name + "*";
+        return all.stream().filter(i -> PatternHelper.matchPattern(i.name(), 
pattern)).toList();
+    }
+
+    /**
+     * Whether the file in the Camel directory is a log of a previous run, or 
what it says about that run.
+     */
+    public static boolean isPreviousRunFile(String fileName) {
+        return fileName.endsWith(LOG_SUFFIX) || fileName.endsWith(INFO_SUFFIX);
+    }
+
+    /**
+     * Keeps the logs that processes which are gone left behind (they were 
killed, or their JVM crashed), under the name
+     * in their status file. A log older than the previous run of its name is 
removed, and so is a log without a name
+     * that is older than a day.
+     */
+    public static void keepOrphans() {
+        File[] files = CommandLineHelper.getCamelDir().toFile().listFiles((d, 
n) -> PID_LOG.matcher(n).matches());
+        if (files == null) {
+            return;
+        }
+        long now = System.currentTimeMillis();
+        for (File f : files) {
+            Matcher m = PID_LOG.matcher(f.getName());
+            if (!m.matches()) {
+                continue;
+            }
+            long pid;
+            try {
+                pid = Long.parseLong(m.group(1));
+            } catch (NumberFormatException e) {
+                continue;
+            }
+            if 
(ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false)) {
+                // still running (or its pid is taken by another process)
+                continue;
+            }
+            long modified = f.lastModified();
+            JsonObject status = 
readJson(CommandLineHelper.getCamelDir().resolve(pid + "-status.json"));
+            String name = status != null && status.get("context") instanceof 
JsonObject context
+                    ? context.getString("name") : null;
+            if (name != null && !name.isBlank()) {
+                Info before = 
read(CommandLineHelper.getCamelDir().resolve(fileName(name) + INFO_SUFFIX));
+                if (before == null || before.ended() < modified) {

Review Comment:
   Right that the two times can come from different clocks, though not as 
described: for a run kept by its own JVM, `ended` is the clock of the JVM 
(`System.currentTimeMillis()`), and only the log of a killed process uses the 
time of its file. A network file system has a clock of its own, so the 
comparison now uses the time of the kept log file on both sides, in 
777f82f9b41f, with a test where the clock of the JVM is a day ahead.
   
   _Claude Code on behalf of davsclaus_



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PreviousRun.java:
##########
@@ -0,0 +1,423 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.common;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * The log of the previous run of an integration, as <tt>kubectl logs 
--previous</tt> shows the log of the previous
+ * instance of a container in Kubernetes.
+ * <p/>
+ * When <tt>camel run</tt> ends (it was stopped, it failed to start, or it 
stopped by itself), its log is kept as
+ * <tt>~/.camel/&lt;name&gt;-previous.log</tt>, by the name of the 
integration, with
+ * <tt>~/.camel/&lt;name&gt;-previous.json</tt>, which says when and how the 
run ended. The next run of that name that
+ * ends replaces both. Like Kubernetes, at most the last 10 MB of a log are 
kept.
+ * <p/>
+ * A process that is gone without keeping its log (it was killed, or its JVM 
crashed) leaves its
+ * <tt>~/.camel/&lt;pid&gt;.log</tt> behind. That log is kept for it, under 
the name in its status file, the next time
+ * the previous runs are looked up, and the next time an integration is run.
+ */
+public final class PreviousRun {
+
+    /**
+     * The end of a log that is kept, at most: Kubernetes keeps at most 10 MB 
of the log of a container by default.
+     */
+    static final long MAX_SIZE = 10L * 1024 * 1024;
+
+    /**
+     * How old the log of a process that is gone may get, when there is no 
name to keep it by, before it is removed.
+     */
+    static final long ORPHAN_AGE = TimeUnit.DAYS.toMillis(1);
+
+    /**
+     * What a process that is gone without keeping its log ended with, as far 
as is known.
+     */
+    static final String GONE = "the process was gone without stopping Camel 
(killed, or its JVM crashed)";
+
+    private static final String LOG_SUFFIX = "-previous.log";
+    private static final String INFO_SUFFIX = "-previous.json";
+    private static final Pattern PID_LOG = Pattern.compile("(\\d+)\\.log");
+    private static final int MAX_FAILURE = 500;
+    private static final DateTimeFormatter TIME
+            = DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm:ss").withZone(ZoneId.systemDefault());
+
+    /**
+     * A run that ended, and the log it kept.
+     *
+     * @param name      the name of the integration
+     * @param pid       the pid it ran as, 0 when not known
+     * @param directory the directory it ran in, or <tt>null</tt>
+     * @param started   when it started, in epoch millis, 0 when not known
+     * @param ended     when it ended, in epoch millis
+     * @param exitCode  the exit code, or <tt>null</tt> when not known (such 
as of a process that was killed)
+     * @param failure   why it ended, when it failed (such as the error it 
failed to start with), or <tt>null</tt>
+     * @param log       the log it kept
+     */
+    public record Info(String name, long pid, String directory, long started, 
long ended, Integer exitCode,
+            String failure, Path log) {
+
+        /**
+         * Which run it was, and how it ended, in one line.
+         */
+        public String describe() {
+            return "The previous run of " + name + (pid > 0 ? " (pid " + pid + 
")" : "") + " " + howItEnded();
+        }
+
+        /**
+         * How the run ended: when, with which exit code, and why when it 
failed.
+         */
+        public String howItEnded() {
+            StringBuilder sb = new StringBuilder("ended at 
").append(TIME.format(Instant.ofEpochMilli(ended)));
+            long ago = System.currentTimeMillis() - ended;
+            if (ago >= 0) {
+                sb.append(" (").append(TimeUtils.printDuration(ago, 
false)).append(" ago)");
+            }
+            if (exitCode != null) {
+                sb.append(" with exit code ").append(exitCode);
+            }
+            if (failure != null) {
+                sb.append(": ").append(failure);
+            }
+            return sb.toString();
+        }
+    }
+
+    private PreviousRun() {
+    }
+
+    /**
+     * Keeps the log of a run of this JVM that ended as the previous log of 
its name.
+     *
+     * @param  log      the log of the run
+     * @param  name     the name of the integration
+     * @param  pid      the pid it ran as, 0 when not known
+     * @param  exitCode the exit code, or <tt>null</tt> when not known
+     * @param  failure  why it ended, when it failed, or <tt>null</tt>
+     * @return          the kept log, or <tt>null</tt> when there was no log 
to keep
+     */
+    public static Path keep(Path log, String name, long pid, Integer exitCode, 
String failure) {
+        long started = 
ProcessHandle.current().info().startInstant().map(Instant::toEpochMilli).orElse(0L);
+        return keep(log, new Info(
+                name, pid, System.getProperty("user.dir"), started, 
System.currentTimeMillis(),
+                exitCode, failure, null));
+    }
+
+    /**
+     * Keeps the log of a run that ended as the previous log of its name, in 
place of the one kept before.
+     *
+     * @return the kept log, or <tt>null</tt> when there was no log to keep
+     */
+    static Path keep(Path log, Info info) {
+        if (log == null || info.name() == null || info.name().isBlank() || 
!Files.isRegularFile(log)) {
+            return null;
+        }
+        Path dir = CommandLineHelper.getCamelDir();
+        String file = fileName(info.name());
+        Path target = dir.resolve(file + LOG_SUFFIX);
+        try {
+            if (Files.size(log) > MAX_SIZE || !move(log, target)) {
+                copyEnd(log, target, MAX_SIZE);
+                try {
+                    Files.deleteIfExists(log);
+                } catch (IOException e) {
+                    // still open (Windows): removed when the process is gone 
(see keepOrphans)
+                }
+            }
+            JsonObject jo = new JsonObject();
+            jo.put("name", info.name());
+            if (info.pid() > 0) {
+                jo.put("pid", info.pid());
+            }
+            if (info.directory() != null) {
+                jo.put("directory", info.directory());
+            }
+            if (info.started() > 0) {
+                jo.put("started", info.started());
+            }
+            jo.put("ended", info.ended());
+            if (info.exitCode() != null) {
+                jo.put("exitCode", info.exitCode());
+            }
+            if (info.failure() != null) {
+                String failure = info.failure().strip();
+                jo.put("failure", failure.length() > MAX_FAILURE ? 
failure.substring(0, MAX_FAILURE) + "..." : failure);
+            }
+            Files.writeString(dir.resolve(file + INFO_SUFFIX), jo.toJson());
+            return target;
+        } catch (IOException e) {

Review Comment:
   Checked: the original log survives, as you say. But the failed copy had 
already truncated the old `<name>-previous.log`, and the old 
`<name>-previous.json` stayed next to it, describing another run (the same when 
the move works and writing the JSON fails). In 777f82f9b41f the old JSON is 
deleted first, so a keep that fails leaves no stale info; tested with a target 
that cannot be written. I left out the debug log: this class has no logger, and 
in `camel run` the log it would write to is the file being moved.
   
   _Claude Code on behalf of davsclaus_



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogAction.java:
##########
@@ -217,6 +232,60 @@ public Integer doCall() throws Exception {
         return 0;
     }
 
+    /**
+     * The previous run of the integration (CAMEL-25784): the one that matches 
the name, after a line on stderr that
+     * says which run it was and how it ended.
+     *
+     * @throws CliException with exit code 3 when no previous run, or more 
than one, matches
+     */
+    private void previousRun(Map<Long, Row> rows) {
+        boolean all = name == null || name.isBlank() || "*".equals(name);
+        List<PreviousRun.Info> found = PreviousRun.find(name);
+        if (found.isEmpty()) {
+            throw CliException.notFound(all ? "No previous run of a Camel 
integration" : "No previous run matches: " + name);
+        }
+        if (found.size() > 1) {
+            StringJoiner names = new StringJoiner(", ");
+            found.forEach(i -> names.add(i.name()));
+            throw CliException.notFound((all ? "There are " : "Name " + name + 
" matches ") + found.size()
+                                        + " previous runs: " + names + ". 
Specify the name of one.");
+        }
+        PreviousRun.Info info = found.get(0);
+        printer().printDiagnostic(info.describe());
+        Row row = new Row();
+        row.pid = Long.toString(info.pid());
+        row.name = info.name();
+        row.file = info.log();
+        nameMaxWidth = Math.max(NAME_MIN_WIDTH, Math.min(NAME_MAX_WIDTH, 
row.name.length()));
+        rows.put(info.pid(), row);
+    }
+
+    /**
+     * Fails when a name given matches no running integration, saying where 
the log of its previous run is, which tells
+     * why it is not running.
+     */
+    private void findRunning() {
+        try {
+            if (findProcesses(name).isEmpty()) {
+                // nothing runs
+                List<PreviousRun.Info> found = PreviousRun.findAll();
+                if (!found.isEmpty()) {
+                    printer().printTip(previousRunHint(found.get(0)));

Review Comment:
   Confirmed: `findAll()` sorts by when the run ended, newest first, as its 
javadoc says. The comment at the call site says so now, in 777f82f9b41f.
   
   _Claude Code on behalf of davsclaus_



-- 
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