From c60488953298ee58cca3a85cf4e100dc30d713c5 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Wed, 18 Sep 2024 12:30:06 -0400 Subject: [PATCH] `TailLog` to support reopened files --- .../java/org/jvnet/hudson/test/TailLog.java | 49 ++++++++++++- .../org/jvnet/hudson/test/TailLogTest.java | 68 +++++++++++++++++++ 2 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/jvnet/hudson/test/TailLogTest.java diff --git a/src/main/java/org/jvnet/hudson/test/TailLog.java b/src/main/java/org/jvnet/hudson/test/TailLog.java index 0a5fee51b..6c4434eb6 100644 --- a/src/main/java/org/jvnet/hudson/test/TailLog.java +++ b/src/main/java/org/jvnet/hudson/test/TailLog.java @@ -29,8 +29,13 @@ import hudson.model.Job; import hudson.model.Run; import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Duration; import java.util.concurrent.Semaphore; import org.apache.commons.io.input.Tailer; import org.apache.commons.io.input.TailerListenerAdapter; @@ -94,7 +99,45 @@ public TailLog withColor(PrefixedOutputStream.AnsiColor color) { * @param job a {@link Job#getFullName} */ public TailLog(File buildDirectory, String job, int number) { - tailer = Tailer.create(new File(buildDirectory, "log"), new TailerListenerAdapter() { + var log = buildDirectory.toPath().resolve("log"); + tailer = Tailer.builder().setDelayDuration(Duration.ofMillis(50)).setTailable(new Tailer.Tailable() { + // like TailablePath + @Override public long size() throws IOException { + return Files.size(log); + } + @Override public FileTime lastModifiedFileTime() throws IOException { + return Files.getLastModifiedTime(log); + } + @Override public boolean isNewer(FileTime fileTime) throws IOException { + return Files.getLastModifiedTime(log).compareTo(fileTime) > 0; + } + @Override public Tailer.RandomAccessResourceBridge getRandomAccess(String mode) throws FileNotFoundException { + if (!Files.isRegularFile(log)) { + throw new FileNotFoundException(log.toString()); + } + return new Tailer.RandomAccessResourceBridge() { + long ptr; + @Override public long getPointer() throws IOException { + return ptr; + } + @Override public void seek(long pos) throws IOException { + ptr = pos; + } + @Override public int read(byte[] b) throws IOException { + // Unlike RandomAccessFileBridge, not sensitive to file handle: + try (var is = Files.newInputStream(log)) { + is.skipNBytes(ptr); + int r = is.read(b); + if (r > 0) { + ptr += r; + } + return r; + } + } + @Override public void close() throws IOException {} + }; + } + }).setTailerListener(new TailerListenerAdapter() { PrintStream ps; @Override public void handle(String line) { @@ -112,7 +155,7 @@ public void handle(String line) { finished.release(); } } - }, 50); + }).get(); } public void waitForCompletion() throws InterruptedException { @@ -121,7 +164,7 @@ public void waitForCompletion() throws InterruptedException { @Override public void close() { - tailer.stop(); + tailer.close(); } } diff --git a/src/test/java/org/jvnet/hudson/test/TailLogTest.java b/src/test/java/org/jvnet/hudson/test/TailLogTest.java new file mode 100644 index 000000000..a897759c3 --- /dev/null +++ b/src/test/java/org/jvnet/hudson/test/TailLogTest.java @@ -0,0 +1,68 @@ +/* + * The MIT License + * + * Copyright 2024 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jvnet.hudson.test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.PrintWriter; +import org.apache.commons.io.FileUtils; +import static org.junit.Assert.assertTrue; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public final class TailLogTest { + + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + + @Test public void recreatedLog() throws Exception { + var dir = tmp.getRoot(); + try (var tail = new TailLog(dir, "prj", 123).withColor(PrefixedOutputStream.Color.MAGENTA)) { + Thread.sleep(1000); + var log = new File(dir, "log"); + try (var os = new FileOutputStream(log); var pw = new PrintWriter(os)) { + for (int i = 0; i < 10; i++) { + pw.println(i); + pw.flush(); + Thread.sleep(500); + } + } + var log2 = new File(dir, "log.tmp"); + FileUtils.copyFile(log, log2); + FileUtils.delete(log); + assertTrue(log2.renameTo(log)); + try (var os = new FileOutputStream(log, true); var pw = new PrintWriter(os)) { + for (int i = 10; i < 20; i++) { + pw.println(i); + pw.flush(); + Thread.sleep(500); + } + pw.println("Finished: WHATEVER"); + } + tail.waitForCompletion(); + } + } + +}