Skip to content

Commit 0aaf80e

Browse files
ctruedenclaude
andcommitted
Give each Task a Logger for its output
Tasks such as building a Python environment or running an external process produce output that users sometimes need to see, but which does not fit into a status message. Task#log() provides a channel for it, which UIs can listen to via Logger#addLogListener. Task output is kept separate from the application log, so that e.g. a package manager's chatter does not flood the console. DefaultTask's logger passes every level to its listeners, and retains nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5dba100 commit 0aaf80e

4 files changed

Lines changed: 104 additions & 1 deletion

File tree

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</parent>
1111

1212
<artifactId>scijava-common</artifactId>
13-
<version>2.100.2-SNAPSHOT</version>
13+
<version>2.101.0-SNAPSHOT</version>
1414

1515
<name>SciJava Common</name>
1616
<description>SciJava Common is a shared library for SciJava software. It provides a plugin framework, with an extensible mechanism for service discovery, backed by its own annotation processor, so that plugins can be loaded dynamically. It is used by downstream projects in the SciJava ecosystem, such as ImageJ and SCIFIO.</description>

‎src/main/java/org/scijava/task/DefaultTask.java‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
import java.util.concurrent.Future;
3333

3434
import org.scijava.event.EventService;
35+
import org.scijava.log.DefaultLogger;
36+
import org.scijava.log.LogLevel;
37+
import org.scijava.log.LogSource;
38+
import org.scijava.log.Logger;
3539
import org.scijava.task.event.TaskEvent;
3640
import org.scijava.thread.ThreadService;
3741

@@ -85,6 +89,8 @@ public class DefaultTask implements Task {
8589

8690
private Runnable cancelCallBack;
8791

92+
private volatile Logger logger;
93+
8894
/**
8995
* Creates a new task.
9096
*
@@ -199,6 +205,32 @@ public Runnable getCancelCallBack() {
199205
return this.cancelCallBack;
200206
}
201207

208+
/**
209+
* {@inheritDoc}
210+
* <p>
211+
* The logger passes messages of every level to its listeners, and retains
212+
* nothing: listeners receive only what is logged after they are added.
213+
* </p>
214+
*/
215+
@Override
216+
public Logger log() {
217+
// Note: Double-checked locking, so that logging many lines does not
218+
// mean acquiring a lock for each one.
219+
Logger result = logger;
220+
if (result == null) {
221+
synchronized (this) {
222+
result = logger;
223+
if (result == null) {
224+
final String source = name == null ? "task" : name;
225+
result = new DefaultLogger(message -> {}, LogSource.newRoot()
226+
.subSource(source), LogLevel.TRACE);
227+
logger = result;
228+
}
229+
}
230+
}
231+
return result;
232+
}
233+
202234
@Override
203235
public String getCancelReason() {
204236
return cancelReason;

‎src/main/java/org/scijava/task/Task.java‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333

3434
import org.scijava.Cancelable;
3535
import org.scijava.Named;
36+
import org.scijava.log.DefaultLogger;
37+
import org.scijava.log.LogLevel;
38+
import org.scijava.log.LogSource;
39+
import org.scijava.log.Logger;
3640

3741
/**
3842
* A self-aware job which reports its status and progress as it runs.
@@ -151,4 +155,22 @@ default Runnable getCancelCallBack() {
151155
return () -> {};
152156
}
153157

158+
/**
159+
* Gets the logger for output produced while performing the task, such as
160+
* the lines printed by an external process it runs. Called by task
161+
* implementors to emit output, and by interested parties (e.g. a UI showing
162+
* the task) to listen for it via {@link Logger#addLogListener}.
163+
* <p>
164+
* Task output is kept separate from the application log: messages logged
165+
* here reach this logger's listeners only.
166+
* </p>
167+
* <p>
168+
* The default implementation returns a logger that discards everything.
169+
* </p>
170+
*/
171+
default Logger log() {
172+
return new DefaultLogger(message -> {}, LogSource.newRoot(),
173+
LogLevel.NONE);
174+
}
175+
154176
}

‎src/test/java/org/scijava/task/TaskServiceTest.java‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,20 @@
3030
package org.scijava.task;
3131

3232
import static org.junit.Assert.assertEquals;
33+
import static org.junit.Assert.assertSame;
34+
import static org.junit.Assert.assertTrue;
3335

36+
import java.util.ArrayList;
37+
import java.util.List;
3438
import java.util.concurrent.ExecutionException;
3539

3640
import org.junit.After;
3741
import org.junit.Before;
3842
import org.junit.Test;
3943
import org.scijava.Context;
44+
import org.scijava.log.LogLevel;
45+
import org.scijava.log.LogMessage;
46+
import org.scijava.log.Logger;
4047

4148
/**
4249
* Tests {@link TaskService}.
@@ -71,4 +78,46 @@ public void testTask() throws InterruptedException, ExecutionException {
7178
task.waitFor();
7279
assertEquals(100, result[0]);
7380
}
81+
82+
@Test
83+
public void testLogger() {
84+
final Task task = taskService.createTask("chatty");
85+
final Logger logger = task.log();
86+
assertSame(logger, task.log());
87+
assertEquals("chatty", logger.getSource().name());
88+
89+
final List<LogMessage> messages = new ArrayList<>();
90+
logger.addLogListener(messages::add);
91+
logger.debug("detail");
92+
logger.warn("careful");
93+
assertEquals(2, messages.size());
94+
assertEquals("detail", messages.get(0).text());
95+
assertEquals(LogLevel.DEBUG, messages.get(0).level());
96+
assertEquals(LogLevel.WARN, messages.get(1).level());
97+
}
98+
99+
@Test
100+
public void testDefaultLoggerDiscards() {
101+
final Task task = new Task() {
102+
@Override public void run(final Runnable r) {}
103+
@Override public void waitFor() {}
104+
@Override public boolean isDone() { return false; }
105+
@Override public String getStatusMessage() { return null; }
106+
@Override public long getProgressValue() { return 0; }
107+
@Override public long getProgressMaximum() { return 0; }
108+
@Override public void setStatusMessage(final String status) {}
109+
@Override public void setProgressValue(final long step) {}
110+
@Override public void setProgressMaximum(final long max) {}
111+
@Override public boolean isCanceled() { return false; }
112+
@Override public void cancel(final String reason) {}
113+
@Override public String getCancelReason() { return null; }
114+
@Override public String getName() { return null; }
115+
@Override public void setName(final String name) {}
116+
};
117+
final Logger logger = task.log();
118+
final List<LogMessage> messages = new ArrayList<>();
119+
logger.addLogListener(messages::add);
120+
logger.error("ignored");
121+
assertTrue(messages.isEmpty());
122+
}
74123
}

0 commit comments

Comments
 (0)