Skip to content

Commit 6aaaa90

Browse files
web-dev0521bartlomieju
andauthored
feat(task): prefix output lines with task name when running in parallel (#33805)
Closes #27647 When running multiple tasks in parallel via `deno task` (e.g. `"dev": { "dependencies": ["task_a", "task_b"] }`), all stdout/stderr streams to the terminal without labels, making interleaved output impossible to attribute. This PR prefixes each output line with the task name so it is always clear which task produced which output. **Before:** ``` Task build tsc && node scripts/bundle.js Task test jest --watch src/index.ts PASS src/index.test.ts Compilation complete. Test Suites: 1 passed ``` **After:** ``` Task build tsc && node scripts/bundle.js Task test jest --watch [build] src/index.ts [test] PASS src/index.test.ts [build] Compilation complete. [test] Test Suites: 1 passed ``` --------- Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
1 parent 443a427 commit 6aaaa90

16 files changed

Lines changed: 336 additions & 37 deletions

File tree

cli/args/flags.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ pub struct TaskFlags {
569569
pub recursive: bool,
570570
pub filter: Option<String>,
571571
pub eval: bool,
572+
pub no_prefix: bool,
572573
}
573574

574575
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@@ -4439,6 +4440,14 @@ Evaluate a task from string:
44394440
"Evaluate the passed value as if it was a task in a configuration file",
44404441
).action(ArgAction::SetTrue)
44414442
)
4443+
.arg(
4444+
Arg::new("no-prefix")
4445+
.long("no-prefix")
4446+
.help(
4447+
"Disable prefixing the output of concurrently-executing tasks with the task name",
4448+
)
4449+
.action(ArgAction::SetTrue),
4450+
)
44424451
.arg(node_modules_dir_arg())
44434452
.arg(node_modules_linker_arg())
44444453
.arg(tunnel_arg())
@@ -7610,6 +7619,7 @@ fn task_parse(
76107619
recursive,
76117620
filter,
76127621
eval: matches.get_flag("eval"),
7622+
no_prefix: matches.get_flag("no-prefix"),
76137623
};
76147624

76157625
match matches.remove_subcommand() {
@@ -13480,6 +13490,7 @@ mod tests {
1348013490
recursive: false,
1348113491
filter: None,
1348213492
eval: false,
13493+
no_prefix: false,
1348313494
}),
1348413495
argv: svec!["hello", "world"],
1348513496
..Flags::default()
@@ -13497,6 +13508,7 @@ mod tests {
1349713508
recursive: false,
1349813509
filter: None,
1349913510
eval: false,
13511+
no_prefix: false,
1350013512
}),
1350113513
..Flags::default()
1350213514
}
@@ -13513,6 +13525,7 @@ mod tests {
1351313525
recursive: false,
1351413526
filter: None,
1351513527
eval: false,
13528+
no_prefix: false,
1351613529
}),
1351713530
..Flags::default()
1351813531
}
@@ -13529,6 +13542,7 @@ mod tests {
1352913542
recursive: false,
1353013543
filter: Some("*".to_string()),
1353113544
eval: false,
13545+
no_prefix: false,
1353213546
}),
1353313547
..Flags::default()
1353413548
}
@@ -13545,6 +13559,7 @@ mod tests {
1354513559
recursive: true,
1354613560
filter: Some("*".to_string()),
1354713561
eval: false,
13562+
no_prefix: false,
1354813563
}),
1354913564
..Flags::default()
1355013565
}
@@ -13561,6 +13576,7 @@ mod tests {
1356113576
recursive: true,
1356213577
filter: Some("*".to_string()),
1356313578
eval: false,
13579+
no_prefix: false,
1356413580
}),
1356513581
..Flags::default()
1356613582
}
@@ -13577,6 +13593,7 @@ mod tests {
1357713593
recursive: false,
1357813594
filter: None,
1357913595
eval: true,
13596+
no_prefix: false,
1358013597
}),
1358113598
..Flags::default()
1358213599
}
@@ -13608,6 +13625,7 @@ mod tests {
1360813625
recursive: false,
1360913626
filter: None,
1361013627
eval: false,
13628+
no_prefix: false,
1361113629
}),
1361213630
argv: svec!["--", "hello", "world"],
1361313631
config_flag: ConfigFlag::Path("deno.json".to_owned()),
@@ -13628,6 +13646,7 @@ mod tests {
1362813646
recursive: false,
1362913647
filter: None,
1363013648
eval: false,
13649+
no_prefix: false,
1363113650
}),
1363213651
argv: svec!["--", "hello", "world"],
1363313652
..Flags::default()
@@ -13649,6 +13668,7 @@ mod tests {
1364913668
recursive: false,
1365013669
filter: None,
1365113670
eval: false,
13671+
no_prefix: false,
1365213672
}),
1365313673
argv: svec!["--"],
1365413674
..Flags::default()
@@ -13669,6 +13689,7 @@ mod tests {
1366913689
recursive: false,
1367013690
filter: None,
1367113691
eval: false,
13692+
no_prefix: false,
1367213693
}),
1367313694
argv: svec!["-1", "--test"],
1367413695
..Flags::default()
@@ -13689,6 +13710,7 @@ mod tests {
1368913710
recursive: false,
1369013711
filter: None,
1369113712
eval: false,
13713+
no_prefix: false,
1369213714
}),
1369313715
argv: svec!["--test"],
1369413716
..Flags::default()
@@ -13710,6 +13732,7 @@ mod tests {
1371013732
recursive: false,
1371113733
filter: None,
1371213734
eval: false,
13735+
no_prefix: false,
1371313736
}),
1371413737
log_level: Some(log::Level::Error),
1371513738
..Flags::default()
@@ -13730,6 +13753,7 @@ mod tests {
1373013753
recursive: false,
1373113754
filter: None,
1373213755
eval: false,
13756+
no_prefix: false,
1373313757
}),
1373413758
..Flags::default()
1373513759
}
@@ -13749,6 +13773,7 @@ mod tests {
1374913773
recursive: false,
1375013774
filter: None,
1375113775
eval: false,
13776+
no_prefix: false,
1375213777
}),
1375313778
config_flag: ConfigFlag::Path("deno.jsonc".to_string()),
1375413779
..Flags::default()
@@ -13769,6 +13794,7 @@ mod tests {
1376913794
recursive: false,
1377013795
filter: None,
1377113796
eval: false,
13797+
no_prefix: false,
1377213798
}),
1377313799
config_flag: ConfigFlag::Path("deno.jsonc".to_string()),
1377413800
..Flags::default()

cli/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ async fn run_subcommand(
274274
recursive: false,
275275
filter: None,
276276
eval: false,
277+
no_prefix: false,
277278
};
278279
let mut flags = flags;
279280
flags.subcommand = DenoSubcommand::Task(task_flags.clone());
@@ -381,6 +382,7 @@ async fn run_subcommand(
381382
recursive: false,
382383
filter: None,
383384
eval: false,
385+
no_prefix: false,
384386
};
385387
new_flags.subcommand = DenoSubcommand::Task(task_flags.clone());
386388
let result = tools::task::execute_script(

cli/task_runner.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::collections::HashMap;
44
use std::ffi::OsStr;
55
use std::ffi::OsString;
6+
use std::io::Write;
67
use std::path::Path;
78
use std::path::PathBuf;
89
use std::rc::Rc;
@@ -41,6 +42,85 @@ pub fn get_script_with_args(script: &str, argv: &[String]) -> String {
4142

4243
pub struct TaskStdio(Option<ShellPipeReader>, ShellPipeWriter);
4344

45+
pub struct PrefixedWriter<W: std::io::Write> {
46+
prefix: Vec<u8>,
47+
inner: W,
48+
line_buf: Vec<u8>,
49+
at_line_start: bool,
50+
}
51+
52+
impl<W: std::io::Write> PrefixedWriter<W> {
53+
pub fn new(prefix: String, inner: W) -> Self {
54+
Self {
55+
prefix: prefix.into_bytes(),
56+
inner,
57+
line_buf: Vec::new(),
58+
at_line_start: true,
59+
}
60+
}
61+
}
62+
63+
impl<W: std::io::Write> std::io::Write for PrefixedWriter<W> {
64+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
65+
let mut rest = buf;
66+
while !rest.is_empty() {
67+
if self.at_line_start {
68+
self.line_buf.extend_from_slice(&self.prefix);
69+
self.at_line_start = false;
70+
}
71+
match rest.iter().position(|&b| b == b'\n') {
72+
Some(pos) => {
73+
self.line_buf.extend_from_slice(&rest[..pos + 1]);
74+
self.inner.write_all(&self.line_buf)?;
75+
self.line_buf.clear();
76+
self.at_line_start = true;
77+
rest = &rest[pos + 1..];
78+
}
79+
None => {
80+
self.line_buf.extend_from_slice(rest);
81+
break;
82+
}
83+
}
84+
}
85+
Ok(buf.len())
86+
}
87+
88+
fn flush(&mut self) -> std::io::Result<()> {
89+
if !self.line_buf.is_empty() {
90+
self.inner.write_all(&self.line_buf)?;
91+
self.line_buf.clear();
92+
self.at_line_start = true;
93+
}
94+
self.inner.flush()
95+
}
96+
}
97+
98+
pub fn make_prefixed_task_io(prefix: String) -> (TaskIo, Vec<JoinHandle<()>>) {
99+
let (out_r, out_w) = deno_task_shell::pipe();
100+
let (err_r, err_w) = deno_task_shell::pipe();
101+
102+
let out_prefix = prefix.clone();
103+
let out_handle = tokio::task::spawn_blocking(move || {
104+
let mut writer = PrefixedWriter::new(out_prefix, std::io::stdout());
105+
let _ = out_r.pipe_to(&mut writer);
106+
let _ = writer.flush();
107+
});
108+
109+
let err_handle = tokio::task::spawn_blocking(move || {
110+
let mut writer = PrefixedWriter::new(prefix, std::io::stderr());
111+
let _ = err_r.pipe_to(&mut writer);
112+
let _ = writer.flush();
113+
});
114+
115+
(
116+
TaskIo {
117+
stdout: TaskStdio(None, out_w),
118+
stderr: TaskStdio(None, err_w),
119+
},
120+
vec![out_handle, err_handle],
121+
)
122+
}
123+
44124
impl TaskStdio {
45125
pub fn stdout() -> Self {
46126
Self(None, ShellPipeWriter::stdout())

0 commit comments

Comments
 (0)