Skip to content

Commit 4f8a526

Browse files
authored
Add external processor support (#705)
* Add external processor support * CHANGELOG.md * Clippy * Address review comments
1 parent 0641b71 commit 4f8a526

File tree

6 files changed

+188
-4
lines changed

6 files changed

+188
-4
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Allow `partition_table_offset` to be specified in the config file. (for #699)
13+
- Support external log-processors (#705)
1314

1415
### Changed
1516

cargo-espflash/src/main.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ fn flash(args: FlashArgs, config: &Config) -> Result<()> {
323323
build(&args.build_args, &cargo_config, chip).wrap_err("Failed to build project")?;
324324

325325
// Read the ELF data from the build path and load it to the target.
326-
let elf_data = fs::read(build_ctx.artifact_path).into_diagnostic()?;
326+
let elf_data = fs::read(build_ctx.artifact_path.clone()).into_diagnostic()?;
327327

328328
print_board_info(&mut flasher)?;
329329

@@ -368,6 +368,8 @@ fn flash(args: FlashArgs, config: &Config) -> Result<()> {
368368
args.flash_args.monitor_baud.unwrap_or(default_baud),
369369
args.flash_args.log_format,
370370
true,
371+
args.flash_args.processors,
372+
Some(build_ctx.artifact_path),
371373
)
372374
} else {
373375
Ok(())

espflash/src/bin/espflash.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub struct Cli {
3030
subcommand: Commands,
3131

3232
/// Do not check for updates
33-
#[clap(short, long, global = true, action)]
33+
#[clap(short = 'S', long, global = true, action)]
3434
skip_update_check: bool,
3535
}
3636

@@ -303,6 +303,8 @@ fn flash(args: FlashArgs, config: &Config) -> Result<()> {
303303
args.flash_args.monitor_baud.unwrap_or(default_baud),
304304
args.flash_args.log_format,
305305
true,
306+
args.flash_args.processors,
307+
Some(args.image),
306308
)
307309
} else {
308310
Ok(())

espflash/src/cli/mod.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ pub struct FlashArgs {
156156
pub no_skip: bool,
157157
#[clap(flatten)]
158158
pub image: ImageArgs,
159+
/// External log processors to use (comma separated executables)
160+
#[arg(long, requires = "monitor")]
161+
pub processors: Option<String>,
159162
}
160163

161164
/// Operations for partitions tables
@@ -262,6 +265,9 @@ pub struct MonitorArgs {
262265
/// Logging format.
263266
#[arg(long, short = 'L', default_value = "serial", requires = "elf")]
264267
pub log_format: LogFormat,
268+
/// External log processors to use (comma separated executables)
269+
#[arg(long)]
270+
processors: Option<String>,
265271
}
266272

267273
#[derive(Debug, Args)]
@@ -418,7 +424,7 @@ pub fn serial_monitor(args: MonitorArgs, config: &Config) -> Result<()> {
418424
let mut flasher = connect(&args.connect_args, config, true, true)?;
419425
let pid = flasher.get_usb_pid()?;
420426

421-
let elf = if let Some(elf_path) = args.elf {
427+
let elf = if let Some(elf_path) = args.elf.clone() {
422428
let path = fs::canonicalize(elf_path).into_diagnostic()?;
423429
let data = fs::read(path).into_diagnostic()?;
424430

@@ -447,6 +453,8 @@ pub fn serial_monitor(args: MonitorArgs, config: &Config) -> Result<()> {
447453
args.connect_args.baud.unwrap_or(default_baud),
448454
args.log_format,
449455
!args.non_interactive,
456+
args.processors,
457+
args.elf,
450458
)
451459
}
452460

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#![allow(clippy::needless_doctest_main)]
2+
//! External processor support
3+
//!
4+
//! Via the command line argument `--processors` you can instruct espflash to run external executables to pre-process
5+
//! the logs received from the target. Multiple processors are supported by separating them via `,`. Processors are executed in the specified order.
6+
//!
7+
//! You can use full-qualified paths or run an executable which is already in the search path.
8+
//!
9+
//! A processors reads from stdin and output to stdout. Be aware this runs before further processing by espflash.
10+
//! i.e. addresses are not resolved and when using `defmt` you will see encoded data.
11+
//!
12+
//! Additionally be aware that you might receive chunked data which is not always split at valid UTF character boundaries.
13+
//!
14+
//! The executable will get the path of the ELF file as the first argument if available.
15+
//!
16+
//! Example processor which turns some letters into uppercase
17+
//! ```rust,no-run
18+
//! use std::io::{stdin, stdout, Read, Write};
19+
//!
20+
//! fn main() {
21+
//! let args: Vec<String> = std::env::args().collect();
22+
//! println!("ELF file: {:?}", args[1]);
23+
//!
24+
//! let mut buf = [0u8; 1024];
25+
//! loop {
26+
//! if let Ok(len) = stdin().read(&mut buf) {
27+
//! for b in &mut buf[..len] {
28+
//! *b = if b"abdfeo".contains(b) {
29+
//! b.to_ascii_uppercase()
30+
//! } else {
31+
//! *b
32+
//! };
33+
//! }
34+
//!
35+
//! stdout().write(&buf[..len]).unwrap();
36+
//! stdout().flush().unwrap();
37+
//! } else {
38+
//! // ignored
39+
//! }
40+
//! }
41+
//! }
42+
//! ```
43+
44+
use std::{
45+
fmt::Display,
46+
io::{Read, Write},
47+
path::PathBuf,
48+
process::{Child, ChildStdin, Stdio},
49+
sync::mpsc,
50+
};
51+
52+
use miette::Diagnostic;
53+
54+
#[derive(Debug)]
55+
pub struct Error {
56+
executable: String,
57+
}
58+
59+
impl Display for Error {
60+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61+
write!(f, "Failed to launch '{}'", self.executable)
62+
}
63+
}
64+
65+
impl std::error::Error for Error {}
66+
67+
impl Diagnostic for Error {}
68+
69+
struct Processor {
70+
rx: mpsc::Receiver<u8>,
71+
stdin: ChildStdin,
72+
child: Child,
73+
}
74+
75+
impl Processor {
76+
pub fn new(child: Child) -> Self {
77+
let mut child = child;
78+
let (tx, rx) = mpsc::channel::<u8>();
79+
80+
let mut stdout = child.stdout.take().unwrap();
81+
let stdin = child.stdin.take().unwrap();
82+
83+
std::thread::spawn(move || {
84+
let mut buffer = [0u8; 1024];
85+
loop {
86+
if let Ok(len) = stdout.read(&mut buffer) {
87+
for b in &buffer[..len] {
88+
if tx.send(*b).is_err() {
89+
break;
90+
}
91+
}
92+
}
93+
}
94+
});
95+
96+
Self { rx, stdin, child }
97+
}
98+
99+
pub fn try_receive(&mut self) -> Vec<u8> {
100+
let mut res = Vec::new();
101+
while let Ok(b) = self.rx.try_recv() {
102+
res.push(b);
103+
}
104+
res
105+
}
106+
107+
pub fn send(&mut self, data: Vec<u8>) {
108+
let _ignored = self.stdin.write(&data).ok();
109+
}
110+
}
111+
112+
impl Drop for Processor {
113+
fn drop(&mut self) {
114+
self.child.kill().unwrap();
115+
}
116+
}
117+
118+
pub struct ExternalProcessors {
119+
processors: Vec<Processor>,
120+
}
121+
122+
impl ExternalProcessors {
123+
pub fn new(processors: Option<String>, elf: Option<PathBuf>) -> Result<Self, Error> {
124+
let mut args = Vec::new();
125+
126+
if let Some(elf) = elf {
127+
args.push(elf.as_os_str().to_str().unwrap().to_string());
128+
};
129+
130+
let mut spawned = Vec::new();
131+
if let Some(processors) = processors {
132+
for processor in processors.split(",") {
133+
let processor = std::process::Command::new(processor)
134+
.args(args.clone())
135+
.stdin(Stdio::piped())
136+
.stdout(Stdio::piped())
137+
.stderr(Stdio::inherit())
138+
.spawn()
139+
.map_err(|_| Error {
140+
executable: processor.to_string(),
141+
})?;
142+
spawned.push(Processor::new(processor));
143+
}
144+
}
145+
146+
Ok(Self {
147+
processors: spawned,
148+
})
149+
}
150+
151+
pub fn process(&mut self, read: &[u8]) -> Vec<u8> {
152+
let mut buffer = Vec::new();
153+
buffer.extend_from_slice(read);
154+
155+
for processor in &mut self.processors {
156+
processor.send(buffer);
157+
buffer = processor.try_receive();
158+
}
159+
160+
buffer
161+
}
162+
}

espflash/src/cli/monitor/mod.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
1313
use std::{
1414
io::{stdout, ErrorKind, Read, Write},
15+
path::PathBuf,
1516
time::Duration,
1617
};
1718

@@ -20,6 +21,7 @@ use crossterm::{
2021
event::{poll, read, Event, KeyCode, KeyEvent, KeyModifiers},
2122
terminal::{disable_raw_mode, enable_raw_mode},
2223
};
24+
use external_processors::ExternalProcessors;
2325
use log::error;
2426
use miette::{IntoDiagnostic, Result};
2527
#[cfg(feature = "serialport")]
@@ -31,6 +33,7 @@ use crate::{
3133
connection::{reset::reset_after_flash, Port},
3234
};
3335

36+
pub mod external_processors;
3437
pub mod parser;
3538

3639
mod line_endings;
@@ -66,13 +69,16 @@ impl Drop for RawModeGuard {
6669
}
6770

6871
/// Open a serial monitor on the given serial port, using the given input parser.
72+
#[allow(clippy::too_many_arguments)]
6973
pub fn monitor(
7074
mut serial: Port,
7175
elf: Option<&[u8]>,
7276
pid: u16,
7377
baud: u32,
7478
log_format: LogFormat,
7579
interactive_mode: bool,
80+
processors: Option<String>,
81+
elf_file: Option<PathBuf>,
7682
) -> miette::Result<()> {
7783
if interactive_mode {
7884
println!("Commands:");
@@ -101,6 +107,8 @@ pub fn monitor(
101107
LogFormat::Serial => Box::new(parser::serial::Serial),
102108
};
103109

110+
let mut external_processors = ExternalProcessors::new(processors, elf_file)?;
111+
104112
let mut buff = [0; 1024];
105113
loop {
106114
let read_count = match serial.read(&mut buff) {
@@ -110,7 +118,8 @@ pub fn monitor(
110118
err => err.into_diagnostic(),
111119
}?;
112120

113-
parser.feed(&buff[0..read_count], &mut stdout);
121+
let processed = external_processors.process(&buff[0..read_count]);
122+
parser.feed(&processed, &mut stdout);
114123

115124
// Don't forget to flush the writer!
116125
stdout.flush().ok();

0 commit comments

Comments
 (0)