use std::fmt::Write as _;
use crate::store::LogPosition;
pub const SCRIPT: &str = r#"
diag=/home/runner/_diag keep=/home/runner/_diag/.runnerd-pages start_page=$1 start_offset=$2 job= record=
declare -A off
page_of() { local page=${1##*_}; echo "${page%.log}"; }
while :; do
if [ -z "$job" ]; then
for w in "$diag"/Worker_*.log; do
[ -f "$w" ] || continue
id=$(grep -m1 -A1 '"k": "check_run_id"' "$w" | sed -n 's/.*"v": \([0-9]*\).*/\1/p')
run=$(grep -m1 -A1 '"k": "run_id"' "$w" | sed -n 's/.*"v": "\([0-9]*\)".*/\1/p')
record=$(grep -m1 '"jobId": "' "$w" | sed -n 's/.*"jobId": "\([0-9a-f-]*\)".*/\1/p')
# Only now: creating _diag before the runner does would leave it unwritable to the runner.
if [ -n "$id" ] && [ -n "$run" ] && [ -n "$record" ]; then
job=$id
mkdir -p -m 700 "$keep"
echo "@@job $id $run"
fi
done
fi
if [ -n "$job" ]; then
for f in "$diag"/pages/*_"$record"_*.log; do
[ -f "$f" ] && [ "$(page_of "$f")" -ge "$start_page" ] && [ ! -e "$keep/${f##*/}" ] \
&& ln "$f" "$keep/${f##*/}" 2> /dev/null
done
for k in "$keep"/*_"$record"_*.log; do
[ -f "$k" ] || continue
name=${k##*/} page=$(page_of "$k") gone=
if [ "$page" -lt "$start_page" ]; then rm -f "$k"; continue; fi
[ -e "$diag/pages/$name" ] || gone=1
size=$(stat -c %s "$k")
o=${off[$name]:-0}
[ -n "${off[$name]}" ] || [ "$page" -ne "$start_page" ] || o=$start_offset
if [ "$size" -gt "$o" ]; then
echo "@@page $page $o $((size - o))"
tail -c +$((o + 1)) "$k" | head -c $((size - o))
fi
off[$name]=$size
if [ -n "$gone" ]; then echo "@@end $page"; rm -f "$k"; fi
done
fi
sleep 0.2
done
"#;
pub fn command(position: LogPosition) -> Vec<String> {
vec![
"bash".to_owned(),
"-c".to_owned(),
SCRIPT.to_owned(),
"capture".to_owned(),
position.page.to_string(),
position.offset.to_string(),
]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Captured {
Job {
id: u64,
run_id: u64,
},
Lines {
lines: Vec<String>,
position: LogPosition,
},
}
#[derive(Debug, Default)]
pub struct Parser {
input: Vec<u8>,
remaining: usize,
page: u64,
partial_offset: u64,
partial: Vec<u8>,
partial_len: u64,
}
pub const MAX_LINE: usize = 1 << 20;
const BATCH: usize = 1 << 20;
const BOM: &[u8] = "\u{feff}".as_bytes();
fn text(line: &[u8]) -> String {
let line = line.strip_suffix(b"\r").unwrap_or(line);
String::from_utf8_lossy(line.strip_prefix(BOM).unwrap_or(line)).into_owned()
}
impl Parser {
pub fn feed(&mut self, chunk: &[u8]) -> Vec<Captured> {
self.input.extend_from_slice(chunk);
let mut captured = Vec::new();
loop {
if self.remaining > 0 {
if self.input.is_empty() {
break;
}
let take = self.remaining.min(self.input.len());
let data: Vec<u8> = self.input.drain(..take).collect();
self.remaining -= take;
self.data(&data, &mut captured);
continue;
}
let Some(end) = self.input.iter().position(|byte| *byte == b'\n') else {
break;
};
let header: Vec<u8> = self.input.drain(..=end).collect();
self.header(&String::from_utf8_lossy(&header[..end]), &mut captured);
}
captured
}
fn header(&mut self, header: &str, captured: &mut Vec<Captured>) {
let mut words = header.split(' ');
let numbers: Vec<u64> = words
.clone()
.skip(1)
.filter_map(|word| word.parse().ok())
.collect();
match (words.next(), numbers.as_slice()) {
(Some("@@job"), [id, run_id]) => captured.push(Captured::Job {
id: *id,
run_id: *run_id,
}),
(Some("@@page"), [page, offset, length]) => {
if *page != self.page || *offset != self.partial_offset + self.partial_len {
self.page = *page;
self.take_line();
self.partial_offset = *offset;
}
self.remaining = usize::try_from(*length).unwrap_or(usize::MAX);
}
(Some("@@end"), [page]) if *page == self.page && self.partial_len > 0 => {
let line = self.take_line();
self.partial_offset = 0;
captured.push(Captured::Lines {
lines: vec![line],
position: LogPosition {
page: page + 1,
offset: 0,
},
});
}
(Some("@@end"), [page]) if *page == self.page => {
self.partial_offset = 0;
captured.push(Captured::Lines {
lines: Vec::new(),
position: LogPosition {
page: page + 1,
offset: 0,
},
});
}
_ => {}
}
}
fn take_line(&mut self) -> String {
let mut line = text(&self.partial);
let dropped = self.partial_len - self.partial.len() as u64;
if dropped > 0 {
let _ = write!(
line,
" [runnerd: {dropped} more bytes of this line not kept]"
);
}
self.partial.clear();
self.partial_len = 0;
line
}
fn data(&mut self, data: &[u8], captured: &mut Vec<Captured>) {
let mut lines = Vec::new();
let mut bytes = 0;
for (index, segment) in data.split(|byte| *byte == b'\n').enumerate() {
if index > 0 {
let length = self.partial_len + 1;
let line = self.take_line();
self.partial_offset += length;
bytes += line.len() + 1;
lines.push(line);
if bytes >= BATCH {
bytes = 0;
captured.push(Captured::Lines {
lines: std::mem::take(&mut lines),
position: self.position(),
});
}
}
let room = MAX_LINE - self.partial.len();
self.partial
.extend_from_slice(&segment[..room.min(segment.len())]);
self.partial_len += segment.len() as u64;
}
if !lines.is_empty() {
captured.push(Captured::Lines {
lines,
position: self.position(),
});
}
}
fn position(&self) -> LogPosition {
LogPosition {
page: self.page,
offset: self.partial_offset,
}
}
#[cfg(test)]
pub(crate) fn pending(&self) -> usize {
self.partial.capacity()
}
}