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
26 changes: 15 additions & 11 deletions core/exec/src/mill/exec/Execution.scala
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ case class Execution(
def formatHeaderPrefix(countMsg: String, keySuffix: String) =
s"$countMsg$keySuffix${Execution.formatFailedCount(rootFailedCount.get())}"

val tasksTransitive = PlanImpl.transitiveTasks(Seq.from(indexToTerminal)).toSet
val downstreamEdges: Map[Task[?], Set[Task[?]]] =
tasksTransitive.flatMap(t => t.inputs.map(_ -> t)).groupMap(_._1)(_._2)

val allExclusiveCommands = tasksTransitive.filter(_.isExclusiveCommand)
val downstreamOfExclusive =
mill.internal.SpanningForest.breadthFirst[Task[?]](allExclusiveCommands)(t =>
downstreamEdges.getOrElse(t, Set())
)

def evaluateTerminals(
terminals: Seq[Task[?]],
exclusive: Boolean
Expand All @@ -163,9 +173,9 @@ case class Execution(
val group = plan.sortedGroups.lookupKey(terminal)
val exclusiveDeps = deps.filter(d => d.isExclusiveCommand)

if (!terminal.isExclusiveCommand && exclusiveDeps.nonEmpty) {
if (terminal.asCommand.isEmpty && downstreamOfExclusive.contains(terminal)) {
val failure = ExecResult.Failure(
s"Non-exclusive task ${terminal} cannot depend on exclusive task " +
s"Non-Command task ${terminal} cannot depend on exclusive command " +
exclusiveDeps.mkString(", ")
)
val taskResults: Map[Task[?], ExecResult.Failing[Nothing]] = group
Expand Down Expand Up @@ -285,20 +295,14 @@ case class Execution(
terminals.map(t => (t, Await.result(futures(t), duration.Duration.Inf)))
}

val tasks0 = indexToTerminal.filter {
case _: Task.Command[_] => false
case _ => true
}

val tasksTransitive = PlanImpl.transitiveTasks(Seq.from(tasks0)).toSet
val (tasks, leafExclusiveCommands) = indexToTerminal.partition {
case t: Task.Named[_] => tasksTransitive.contains(t) || !t.isExclusiveCommand
val (nonExclusiveTasks, leafExclusiveCommands) = indexToTerminal.partition {
case t: Task.Named[_] => !downstreamOfExclusive.contains(t)
case _ => !serialCommandExec
}

// Run all non-command tasks according to the threads
// given but run the commands in linear order
val nonExclusiveResults = evaluateTerminals(tasks, exclusive = false)
val nonExclusiveResults = evaluateTerminals(nonExclusiveTasks, exclusive = false)

val exclusiveResults = evaluateTerminals(leafExclusiveCommands, exclusive = true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import mill._
import mill.api.Evaluator

def foo = Task { 1 }
def cleanClientWrong(ev: Evaluator) = Task.Command {
clean(ev, "foo")()
def cleanClientWrong = Task {
cleanClientRight()()
println("cleanClientWrong done")
}

def cleanClientRight(ev: Evaluator) = Task.Command(exclusive = true) {
clean(ev, "foo")()
def cleanClientRight() = Task.Command(exclusive = true) {
println("cleanClientRight done")
}

def cleanClientDownstream() = Task.Command() {
cleanClientRight()()
println("cleanClientDownstream done")
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ object NonExclusiveDependsOnExclusive extends UtestIntegrationTestSuite {
val res = tester.eval("cleanClientWrong")
assert(res.isSuccess == false)
assert(res.err.contains(
"Non-exclusive task cleanClientWrong cannot depend on exclusive task clean"
"cleanClientWrong Non-Command task cleanClientWrong cannot depend on exclusive command cleanClientRight"
))
assert(!res.out.contains("cleanClientWrong done"))
}
Expand All @@ -20,5 +20,10 @@ object NonExclusiveDependsOnExclusive extends UtestIntegrationTestSuite {
assert(res.out.contains("cleanClientRight done"))

}
test("downstream") - integrationTest { tester =>
val res = tester.eval("cleanClientDownstream")
assert(res.isSuccess == true)
assert(res.out.contains("cleanClientDownstream done"))
}
}
}
125 changes: 63 additions & 62 deletions integration/feature/full-run-logs/src/FullRunLogsTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ object FullRunLogsTests extends UtestIntegrationTestSuite {

assertGoldenLiteral(
normalize(res.out),
List("<digits>] run <h1>hello</h1>")
List("<h1>hello</h1>")
)

assertGoldenLiteral(
Expand All @@ -58,6 +58,7 @@ object FullRunLogsTests extends UtestIntegrationTestSuite {
"build.mill-<digits>] done compiling",
"<digits>] compile compiling 1 Java source to out/compile.dest/classes ...",
"<digits>] done compiling",
"<digits>] run",
"63/<digits>] ============================== run --text hello =============================="
)
)
Expand Down Expand Up @@ -190,67 +191,67 @@ object FullRunLogsTests extends UtestIntegrationTestSuite {
assertGoldenLiteral(
normalize(res.result.out.text()),
List(
"<digits>] test.run (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_1(X)",
"<digits>] (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_2(X)",
"<digits>] (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_3(X)",
"<digits>] (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_4(X)",
"<digits>] (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_5(X)",
"<digits>] (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_6(X)",
"<digits>] (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_7(X)",
"<digits>] (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_8(X)",
"<digits>] (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_9(X)",
"<digits>] (R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_10(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_11(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_12(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_13(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_14(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_15(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_16(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_17(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_18(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_19(X)",
"<digits>] (G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_20(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_21(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_22(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_23(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_24(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_25(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_26(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_27(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_28(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_29(X)",
"<digits>] (B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_30(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_31(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_32(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_33(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_34(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_35(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_36(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_37(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_38(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_39(X)",
"<digits>] (C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_40(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_41(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_42(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_43(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_44(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_45(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_46(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_47(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_48(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_49(X)",
"<digits>] (M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_50(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_51(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_52(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_53(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_54(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_55(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_56(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_57(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_58(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_59(X)",
"<digits>] (Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_60(X)",
"<digits>] "
"(R)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_1",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_2",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_3",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_4",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_5",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_6",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_7",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_8",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_9",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_10",
"(G)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_11",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_12",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_13",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_14",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_15",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_16",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_17",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_18",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_19",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_20",
"(B)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_21",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_22",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_23",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_24",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_25",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_26",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_27",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_28",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_29",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_30",
"(C)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_31",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_32",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_33",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_34",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_35",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_36",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_37",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_38",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_39",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_40",
"(M)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_41",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_42",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_43",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_44",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_45",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_46",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_47",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_48",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_49",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_50",
"(Y)ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_51",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_52",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_53",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_54",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_55",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_56",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_57",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_58",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_59",
"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ_60",
"(X)"
)
)
// Sometimes order can be mixed up between stdout and stderr, even with mergeErrIntoOut
Expand Down
23 changes: 11 additions & 12 deletions libs/javalib/src/mill/javalib/RunModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ import mill.constants.EnvVars
*/
trait RunModule extends WithJvmWorkerModule with RunModuleApi {

private lazy val bspExt = {
import BspRunModule.given
ModuleRef(this.internalBspRunModule)
}
private lazy val bspExt = ModuleRef(new BspRunModule(this) {}.internalBspRunModule)

private[mill] def bspRunModule: () => BspRunModuleApi = () => bspExt()

Expand Down Expand Up @@ -114,26 +111,28 @@ trait RunModule extends WithJvmWorkerModule with RunModuleApi {
/**
* Runs this module's code in a subprocess and waits for it to finish
*/
def run(args: Task[Args] = Task.Anon(Args())): Task.Command[Unit] = Task.Command {
runForkedTask(finalMainClass, args)()
}
def run(args: Task[Args] = Task.Anon(Args())): Task.Command[Unit] =
Task.Command(exclusive = true) {
runForkedTask(finalMainClass, args)()
}

/**
* Runs this module's code in-process within an isolated classloader. This is
* faster than `run`, but in exchange you have less isolation between runs
* since the code can dirty the parent Mill process and potentially leave it
* in a bad state.
*/
def runLocal(args: Task[Args] = Task.Anon(Args())): Task.Command[Unit] = Task.Command {
runLocalTask(finalMainClass, args)()
}
def runLocal(args: Task[Args] = Task.Anon(Args())): Task.Command[Unit] =
Task.Command(exclusive = true) {
runLocalTask(finalMainClass, args)()
}

/**
* Same as `run`, but lets you specify a main class to run
*/
def runMain(@arg(positional = true) mainClass: String, args: String*): Task.Command[Unit] = {
val task = runForkedTask(Task.Anon { mainClass }, Task.Anon { Args(args) })
Task.Command { task() }
Task.Command(exclusive = true) { task() }
}

/**
Expand All @@ -152,7 +151,7 @@ trait RunModule extends WithJvmWorkerModule with RunModuleApi {
*/
def runMainLocal(@arg(positional = true) mainClass: String, args: String*): Task.Command[Unit] = {
val task = runLocalTask(Task.Anon { mainClass }, Task.Anon { Args(args) })
Task.Command { task() }
Task.Command(exclusive = true) { task() }
}

/**
Expand Down
Loading
Loading