Idle probe: inline pane_id + watch list into bash script (drop positional args)

Root cause of "filter never suppresses": passing the target pane_id and
watch names as positional args to `bash -c "..." _ <id> <names>` had
them silently dropped by wsl.exe's arg-passing layer. Inside bash, $1
and $@ were empty — the script always looked for `TILETOPIA_PANE_ID=`
(no value), found nothing, exited 1.

Fix: format the script string in Rust with pane_id and watch names
already substituted. No positional args to bash → nothing for wsl.exe
to drop. Both inputs are safe to inline (u64 and a compile-time const
list); validation needed if user-supplied watch names ever land here.

Two unit tests guard against regressing to the positional-arg shape.
Also dropped the diagnostic info!() spam added during debugging — back
to debug! in the happy path, single concise probed= line on each cache
miss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-26 18:25:55 +01:00
parent 6772b8db37
commit 9931a92c5f

View file

@ -92,6 +92,13 @@ impl ProbeCache {
// Slow path: re-probe. Drop the lock before shelling out so other
// probes aren't blocked.
let running = probe_pane(distro, pane_id, DEFAULT_WATCH_PROCESSES);
tracing::debug!(
target: "tiletopia_lib::probe",
distro = %distro,
pane_id,
running,
"probed"
);
let mut guard = self.cache.lock();
guard.insert(
@ -111,33 +118,29 @@ impl Default for ProbeCache {
}
}
/// Bash one-liner: for each watched process name, `pgrep -x` for it; for
/// each matching PID, check `/proc/<pid>/environ` for an exact
/// `TILETOPIA_PANE_ID=<target>` entry (null-separated, so we `tr` it to
/// newlines and exact-line-match with `grep -xF`). Exit 0 = match, 1 = no
/// match, anything else = probe failure (treated as `false` upstream —
/// see fail-safe note on `is_watch_process_running`).
/// Build a single-line bash script with the target pane_id and watch-list
/// **interpolated directly** into the script text. The earlier design
/// passed these as positional args (`bash -c "..." _ <id> <names...>`)
/// but `wsl.exe`'s arg-passing layer silently dropped everything after the
/// `-c` script string, so `$1`/`$@` were always empty inside bash.
/// Interpolating from Rust sidesteps the whole arg-passing path.
///
/// `bash` (not `sh`) is required for process substitution `< <(pgrep ...)`.
/// Both bash and pgrep are installed by default on every WSL distro
/// tiletopia targets; if a minimal distro is missing them the probe falls
/// to "not running" and the pane goes idle normally (better than the v1
/// fail-safe which kept suppressing forever).
const PROBE_SCRIPT: &str = r#"
target_id="$1"
shift
for name in "$@"; do
while IFS= read -r pid; do
[ -z "$pid" ] && continue
if [ -r "/proc/$pid/environ" ]; then
if tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -qxF "TILETOPIA_PANE_ID=$target_id"; then
exit 0
fi
fi
done < <(pgrep -x "$name" 2>/dev/null)
done
exit 1
"#;
/// Both inputs are safe to inline: `pane_id` is a `u64` (no metachars) and
/// `watched` is a compile-time const list. If future code wires user-supplied
/// process names through here, validate/escape them first.
///
/// Returns exit 0 if any watched process running in this specific pane has
/// the matching `TILETOPIA_PANE_ID` env marker; exit 1 otherwise.
fn build_probe_script(pane_id: u64, watched: &[&str]) -> String {
let watch_list = watched
.iter()
.map(|n| format!("\"{n}\""))
.collect::<Vec<_>>()
.join(" ");
format!(
r#"target_id={pane_id}; for name in {watch_list}; do for pid in $(pgrep -x "$name" 2>/dev/null); do [ -z "$pid" ] && continue; if [ -r "/proc/$pid/environ" ]; then if tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -qxF "TILETOPIA_PANE_ID=$target_id"; then exit 0; fi; fi; done; done; exit 1"#
)
}
fn probe_pane(distro: &str, pane_id: u64, watched: &[&str]) -> bool {
if !cfg!(windows) {
@ -150,19 +153,15 @@ fn probe_pane(distro: &str, pane_id: u64, watched: &[&str]) -> bool {
return false;
}
// Compose args: bash -c <script> _ <pane_id> <watch>...
// The `_` is `$0` for the script, then watch names are `$@`.
let mut args: Vec<String> = vec![
let script = build_probe_script(pane_id, watched);
let args: Vec<String> = vec![
"-d".to_string(),
distro.to_string(),
"--".to_string(),
"bash".to_string(),
"-c".to_string(),
PROBE_SCRIPT.to_string(),
"_".to_string(),
pane_id.to_string(),
script,
];
args.extend(watched.iter().map(|s| s.to_string()));
let out = match crate::pty::quiet_command_pub("wsl.exe")
.args(&args)
@ -172,7 +171,7 @@ fn probe_pane(distro: &str, pane_id: u64, watched: &[&str]) -> bool {
{
Ok(o) => o,
Err(e) => {
tracing::debug!(
tracing::info!(
"probe: wsl.exe spawn for distro={distro:?} pane={pane_id} failed: {e}"
);
return false;
@ -180,8 +179,8 @@ fn probe_pane(distro: &str, pane_id: u64, watched: &[&str]) -> bool {
};
match out.status.code() {
Some(0) => true, // watched process matching this pane found
Some(1) => false, // no match
Some(0) => true,
Some(1) => false,
Some(other) => {
tracing::debug!(
"probe: distro={distro:?} pane={pane_id} bash exit={other} — treating as not-running"
@ -196,3 +195,36 @@ fn probe_pane(distro: &str, pane_id: u64, watched: &[&str]) -> bool {
}
}
}
#[cfg(test)]
mod tests {
use super::build_probe_script;
/// Regression: positional args (`bash -c "..." _ <id> <names>`) were
/// silently dropped by wsl.exe's arg-passing layer, so `$1`/`$@` were
/// always empty inside bash. The script must inline pane_id + watch
/// names directly. Lock that in.
#[test]
fn build_probe_script_inlines_pane_id_and_watch_list() {
let s = build_probe_script(42, &["claude"]);
assert!(s.contains("target_id=42"), "pane_id not inlined: {s}");
assert!(s.contains("\"claude\""), "watch name not inlined: {s}");
// No "$1" or "$@" — those are the trap.
assert!(!s.contains("$1"), "script still references $1: {s}");
assert!(
!s.contains("\"$@\""),
"script still references $@: {s}"
);
// Sanity: matches the exact env marker shape pty.rs sets.
assert!(
s.contains("TILETOPIA_PANE_ID=$target_id"),
"marker lookup malformed: {s}"
);
}
#[test]
fn build_probe_script_multiple_watch_names() {
let s = build_probe_script(7, &["claude", "vim", "less"]);
assert!(s.contains("\"claude\" \"vim\" \"less\""), "watch list bad: {s}");
}
}