Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,18 @@ class AnsiLogObserver implements TraceObserverV2 {
private final static Pattern TAG_REGEX = ~/ \((.+)\)( *)$/
private final static Pattern LBL_REPLACE = ~/ \(.+\) *$/

// OSC 8 hyperlink escape sequences (using BEL as String Terminator)
private final static String HYPERLINK_START = '\033]8;;'
private final static String HYPERLINK_SEP = '\007'
private final static String HYPERLINK_END = '\033]8;;\007'

protected static String hyperlink(String text, String url) {
if( !url )
return text
final href = url.startsWith('/') ? 'file://' + url : url
return HYPERLINK_START + href + HYPERLINK_SEP + text + HYPERLINK_END
}

protected Ansi line(ProgressRecord stats) {
final term = ansi()
final float tot = stats.getTotalCount()
Expand Down Expand Up @@ -443,8 +455,10 @@ class AnsiLogObserver implements TraceObserverV2 {
final numbs = " ${(int)com} of ${(int)tot}".toString()

// Task hash, eg: [fa/71091a]
// make clickable hyperlink to work dir inferred from session workDir and task hash
final hashDisplay = (stats.workDir && !session.config.cleanup) ? hyperlink(hh, stats.workDir) : hh
term.a(Attribute.INTENSITY_FAINT).a('[').reset()
term.fg(Color.BLUE).a(hh).reset()
term.fg(Color.BLUE).a(hashDisplay).reset()
term.a(Attribute.INTENSITY_FAINT).a('] ').reset()

// Only show 'process > ' if the terminal has lots of width
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ProgressRecord implements Cloneable {
final int index
final String name // process name
String hash // current task hash
String workDir // current task work directory URI
String taskName // current task name
int pending // number of new tasks ready to be submitted
int submitted // number of tasks submitted for execution not yet started
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ class WorkflowStats implements Cloneable {
void markSubmitted(TaskRun task) {
final state = getOrCreateRecord(task.processor)
state.hash = task.hashLog
state.workDir = task.workDirStr
state.taskName = task.name
state.pending --
state.submitted ++
Expand Down Expand Up @@ -350,6 +351,7 @@ class WorkflowStats implements Cloneable {
ProgressRecord state = getOrCreateRecord(task.processor)
state.taskName = task.name
state.hash = task.hashLog
state.workDir = task.workDirStr

if( status == TaskStatus.SUBMITTED ) {
state.submitted --
Expand Down Expand Up @@ -401,6 +403,7 @@ class WorkflowStats implements Cloneable {
if( trace ) {
state.cached++
state.hash = task.hashLog
state.workDir = task.workDirStr
state.taskName = task.name
// global counters
this.cachedMillis += getCpuTime(trace)
Expand All @@ -409,6 +412,7 @@ class WorkflowStats implements Cloneable {
else {
state.stored++
state.hash = 'skipped'
state.workDir = null
state.taskName = task.name
}
changeTimestamp = System.currentTimeMillis()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package nextflow.trace

import nextflow.Session
import spock.lang.Specification
import spock.lang.Unroll

Expand Down Expand Up @@ -108,4 +109,63 @@ class AnsiLogObserverTest extends Specification {

}

def 'should create hyperlink' () {
expect:
// Local paths (starting with /) get file:// prefix added automatically
AnsiLogObserver.hyperlink('hash', '/path/to/work') == '\033]8;;file:///path/to/work\007hash\033]8;;\007'
// URLs with schemes are used as-is
AnsiLogObserver.hyperlink('hash', 's3://bucket/path') == '\033]8;;s3://bucket/path\007hash\033]8;;\007'
AnsiLogObserver.hyperlink('hash', 'gs://bucket/path') == '\033]8;;gs://bucket/path\007hash\033]8;;\007'
AnsiLogObserver.hyperlink('hash', 'az://container/path') == '\033]8;;az://container/path\007hash\033]8;;\007'
AnsiLogObserver.hyperlink('text', null) == 'text'
AnsiLogObserver.hyperlink('text', '') == 'text'
}

def 'should render hash as hyperlink when workDir is set' () {
given:
def session = Mock(Session) { getConfig() >> [cleanup: false] }
def observer = new AnsiLogObserver()
observer.@session = session
observer.@labelWidth = 3
observer.@cols = 190
and:
def stats = new ProgressRecord(1, 'foo')
stats.submitted = 1
stats.hash = '4e/486876'
stats.workDir = WORKDIR

when:
def result = observer.line(stats).toString()

then:
result.contains('\033]8;;' + EXPECTED_HREF + '\007')
result.contains('\033]8;;\007')

where:
WORKDIR | EXPECTED_HREF
'/work/4e/486876abc' | 'file:///work/4e/486876abc'
's3://bucket/work/4e/486876abc' | 's3://bucket/work/4e/486876abc'
}

def 'should not render hyperlink when cleanup is enabled' () {
given:
def session = Mock(Session) { getConfig() >> [cleanup: true] }
def observer = new AnsiLogObserver()
observer.@session = session
observer.@labelWidth = 3
observer.@cols = 190
and:
def stats = new ProgressRecord(1, 'foo')
stats.submitted = 1
stats.hash = '4e/486876'
stats.workDir = '/work/4e/486876abc'

when:
def result = observer.line(stats).toString()

then:
// Should NOT contain hyperlink start sequence
!result.contains('\033]8;;')
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class ProgressRecordTest extends Specification {
with(rec) {
index == 10
name == 'foo'
hash == null
workDir == null
pending == 0
submitted == 0
running == 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,11 @@ class WorkflowStatsTest extends Specification {
def PENDING = 10
def SUBMITTED = 20
def HASH = 'xyz'
def WORKDIR = 'file:///work/xy/z123456'
and:
def task = Mock(TaskRun) {
getHashLog() >> HASH
getWorkDirStr() >> WORKDIR
getProcessor() >> Mock(TaskProcessor) { getId() >> 0 }
}
and:
Expand All @@ -247,6 +249,7 @@ class WorkflowStatsTest extends Specification {
rec.submitted == SUBMITTED +1
and:
rec.hash == HASH
rec.workDir == WORKDIR
}

def 'should mark running' () {
Expand Down Expand Up @@ -724,9 +727,11 @@ class WorkflowStatsTest extends Specification {
given:
def CACHED = 10
def STORED = 20
def WORKDIR = 'file:///work/xy/z123456'
and:
def task = Mock(TaskRun) {
getHashLog() >> 'XYZ'
getWorkDirStr() >> WORKDIR
getProcessor() >> Mock(TaskProcessor) { getId() >> 0 }
}
and:
Expand All @@ -749,6 +754,7 @@ class WorkflowStatsTest extends Specification {
stats.cachedDuration == 5.sec
and:
rec.hash == 'XYZ'
rec.workDir == WORKDIR
rec.cached == CACHED +1
rec.stored == STORED
}
Expand All @@ -760,6 +766,7 @@ class WorkflowStatsTest extends Specification {
and:
def task = Mock(TaskRun) {
getHashLog() >> 'XYZ'
getWorkDirStr() >> 'file:///work/xy/z123456'
getProcessor() >> Mock(TaskProcessor) { getId() >> 0 }
}
and:
Expand All @@ -781,6 +788,7 @@ class WorkflowStatsTest extends Specification {
rec.stored == STORED +1
and:
rec.hash == 'skipped'
rec.workDir == null
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class TowerJsonGeneratorTest extends Specification {
progress.get(0) == [
index:1,
name: 'foo',
workDir: null,
pending: 0,
submitted: 0,
running: 0,
Expand All @@ -133,6 +134,7 @@ class TowerJsonGeneratorTest extends Specification {
progress[1] == [
index:2,
name: 'bar',
workDir: null,
pending: 1,
submitted: 2,
running: 3,
Expand Down
Loading