feat(ssh): implement bash history pruning in injection script

- Introduced a mechanism to prune bash history entries within a defined range, enhancing the management of command history.
- Added a new constant for maximum history entries and updated the injection script to utilize this feature.
- Improved the script's logic to ensure proper handling of history entries, including marking the beginning and end of relevant history sections.
This commit is contained in:
Kang
2026-09-03 10:17:23 +08:00
parent 18fdae9b81
commit 2ea9abf0ea
+118 -19
View File
@@ -42,6 +42,7 @@ const READY_FAILED_MARKER_PREFIX: &str = "7777;NyaTermReadyFailed:";
const COMMAND_MARKER_PREFIX: &str = "7777;NyaTermCommand:";
const LEGACY_READY_MARKER_PREFIX: &str = "7777;DflyReady:";
const LEGACY_COMMAND_MARKER_PREFIX: &str = "7777;DflyCommand:";
const BASH_HISTORY_PRUNE_MAX_ENTRIES: usize = 256;
/// Build a session-unique ready marker: `\x1b]7777;NyaTermReady:<id>\x07`.
pub fn build_ready_marker(session_id: &str) -> String {
@@ -86,18 +87,27 @@ pub fn injection_script(shell: ShellKind, ready_marker: &str) -> Option<String>
.replace('\x1b', "\\033")
.replace('\x07', "\\007");
let command_marker = command_marker_for_ready(ready_marker);
let bash_history_token = format!(
"NyaTermHistory:{}",
BASE64_STANDARD.encode(marker_inner(ready_marker))
);
match shell {
ShellKind::Bash => Some(format!(
concat!(
// Keep this as one compound history entry while bounding every
// physical line below remote PTY canonical-input limits.
" {{\n",
// Use one compound entry when cmdhist is enabled while keeping
// every physical line below remote PTY canonical-input limits.
// Explicit boundaries cover shells with `shopt -u cmdhist`.
" : '{history_token}:begin'; {{\n",
" NYATERM_PRUNE_HISTORY=1;\n",
" NYATERM_READY_PENDING=1;\n",
" NYATERM_SKIP_COMMAND_ONCE=1;\n",
" NYATERM_HISTORY_TOKEN='{history_token}';\n",
" NYATERM_HISTORY_BEGIN=\"${{NYATERM_HISTORY_TOKEN}}:begin\";\n",
" NYATERM_HISTORY_END=\"${{NYATERM_HISTORY_TOKEN}}:end\";\n",
" export NYATERM_INJ=1;\n",
" NYATERM_COMMAND_MARKER=\"{}\";\n",
" NYATERM_READY_FAILED_MARKER=\"$(printf '{}')\";\n",
" NYATERM_COMMAND_MARKER=\"{command_marker}\";\n",
" NYATERM_READY_FAILED_MARKER=\"$(printf '{ready_failed_osc}')\";\n",
" NYATERM_LAST_HISTCMD=\"${{HISTCMD-}}\";\n",
" __nyaterm_host(){{ hostname 2>/dev/null || printf localhost; }};\n",
" __nyaterm_ready_failed(){{\n",
@@ -108,16 +118,23 @@ pub fn injection_script(shell: ShellKind, ready_marker: &str) -> Option<String>
" __nyaterm_prune_history(){{\n",
" [ -n \"${{NYATERM_PRUNE_HISTORY:-}}\" ] || return 0;\n",
" unset NYATERM_PRUNE_HISTORY;\n",
" local hline history_number;\n",
" local hline history_number found_end= history_window remaining={history_prune_limit};\n",
" history_window=\"$(HISTTIMEFORMAT= history \"$remaining\" 2>/dev/null || true)\";\n",
" case \"$history_window\" in *\"$NYATERM_HISTORY_BEGIN\"*\"$NYATERM_HISTORY_END\"*) ;; *) NYATERM_LAST_HISTCMD=\"${{HISTCMD-}}\"; return 0;; esac;\n",
" while [ \"$remaining\" -gt 0 ]; do\n",
" hline=\"$(HISTTIMEFORMAT= history 1 2>/dev/null || true)\";\n",
" case \"$hline\" in *NYATERM_PRUNE_HISTORY*|*NYATERM_INJ*|*__nyaterm_prompt*|*NyaTermReady*)\n",
" if [ -z \"$found_end\" ]; then case \"$hline\" in *\"$NYATERM_HISTORY_END\"*) found_end=1;; *) break;; esac; fi;\n",
" history_number=${{hline#\"${{hline%%[![:space:]]*}}\"}}; history_number=${{history_number%%[!0-9]*}};\n",
" [ -z \"$history_number\" ] || history -d \"$history_number\" 2>/dev/null || true;;\n",
" esac;\n",
" [ -n \"$history_number\" ] || break;\n",
" history -d \"$history_number\" 2>/dev/null || break;\n",
" case \"$hline\" in *\"$NYATERM_HISTORY_BEGIN\"*) break;; esac;\n",
" remaining=$((remaining - 1));\n",
" done;\n",
" NYATERM_LAST_HISTCMD=\"${{HISTCMD-}}\";\n",
" }};\n",
" __nyaterm_emit_command(){{\n",
" local histcmd=\"${{HISTCMD-}}\";\n",
" if [ -n \"${{NYATERM_SKIP_COMMAND_ONCE:-}}\" ]; then unset NYATERM_SKIP_COMMAND_ONCE; NYATERM_LAST_HISTCMD=\"$histcmd\"; return 0; fi;\n",
" if [ -n \"$histcmd\" ] && [ \"${{NYATERM_LAST_HISTCMD-}}\" != \"$histcmd\" ]; then\n",
" NYATERM_LAST_HISTCMD=\"$histcmd\";\n",
" local cmd; cmd=\"$(fc -ln -1 2>/dev/null)\";\n",
@@ -189,10 +206,14 @@ pub fn injection_script(shell: ShellKind, ready_marker: &str) -> Option<String>
" }};\n",
" __nyaterm_repair_prompt(){{ local status=$?; __nyaterm_repair_prompt_container || __nyaterm_ready_failed; return \"$status\"; }};\n",
" __nyaterm_install_prompt(){{ __nyaterm_repair_prompt_container; }};\n",
" if __nyaterm_install_prompt; then if [ -n \"${{NYATERM_READY_PENDING:-}}\" ]; then unset NYATERM_READY_PENDING; printf '{}'; fi; else unset NYATERM_READY_PENDING; __nyaterm_ready_failed; fi;\n",
" }}\n",
" if __nyaterm_install_prompt; then __nyaterm_install_ok=1; else __nyaterm_install_ok=0; fi;\n",
" }}; : '{history_token}:end'; __nyaterm_prune_history; if [ \"$__nyaterm_install_ok\" = 1 ]; then if [ -n \"${{NYATERM_READY_PENDING:-}}\" ]; then unset NYATERM_READY_PENDING; printf '{ready_osc}'; fi; else unset NYATERM_READY_PENDING; __nyaterm_ready_failed; fi; unset __nyaterm_install_ok\n",
),
command_marker, ready_failed_osc, ready_osc,
history_token = bash_history_token,
command_marker = command_marker,
ready_failed_osc = ready_failed_osc,
ready_osc = ready_osc,
history_prune_limit = BASH_HISTORY_PRUNE_MAX_ENTRIES,
)),
ShellKind::Zsh => Some(format!(
@@ -926,21 +947,41 @@ fn parse_command_marker(inner: &str, expected_marker: Option<&str>) -> Option<St
#[cfg(test)]
mod tests {
use super::{
CwdPayloadEvent, MANAGED_BLOCK_END, MANAGED_BLOCK_START, OscStripper, ShellKind,
activation_script, build_ready_marker, injection_script, persistent_script,
rc_managed_block, replace_managed_block,
BASH_HISTORY_PRUNE_MAX_ENTRIES, CwdPayloadEvent, MANAGED_BLOCK_END, MANAGED_BLOCK_START,
OscStripper, ShellKind, activation_script, build_ready_marker, injection_script,
persistent_script, rc_managed_block, replace_managed_block,
};
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
#[test]
fn bash_injection_prunes_its_history_entry() {
fn bash_injection_prunes_its_complete_marked_history_range() {
let script = injection_script(ShellKind::Bash, &build_ready_marker("session-1"))
.expect("bash injection script");
assert!(script.contains("NYATERM_PRUNE_HISTORY=1;"));
assert_eq!(
script
.matches("NyaTermHistory:Nzc3NztOeWFUZXJtUmVhZHk6c2Vzc2lvbi0x:begin")
.count(),
1
);
assert_eq!(
script
.matches("NyaTermHistory:Nzc3NztOeWFUZXJtUmVhZHk6c2Vzc2lvbi0x:end")
.count(),
1
);
assert!(script.contains("while [ \"$remaining\" -gt 0 ]; do"));
assert!(script.contains("history_window=\"$(HISTTIMEFORMAT= history"));
assert!(script.contains("*\"$NYATERM_HISTORY_BEGIN\"*\"$NYATERM_HISTORY_END\"*)"));
assert!(script.contains("*\"$NYATERM_HISTORY_END\"*) found_end=1"));
assert!(script.contains("*\"$NYATERM_HISTORY_BEGIN\"*) break"));
assert!(script.contains("history_number=${hline#"));
assert!(script.contains("history -d \"$history_number\" 2>/dev/null || true;"));
assert!(script.contains("history -d \"$history_number\" 2>/dev/null || break;"));
assert!(script.contains("NYATERM_SKIP_COMMAND_ONCE=1;"));
assert!(script.contains("unset NYATERM_SKIP_COMMAND_ONCE"));
assert!(script.lines().count() < BASH_HISTORY_PRUNE_MAX_ENTRIES);
assert!(!script.contains("BASH_REMATCH"));
assert!(script.contains("if declare -F __nyaterm_prompt"));
assert!(script.contains("__nyaterm_exported_prompt_fallback"));
@@ -973,8 +1014,66 @@ mod tests {
}
let bash = injection_script(ShellKind::Bash, &ready).expect("Bash injection script");
assert!(bash.starts_with(" {\n"));
assert!(bash.ends_with(" }\n"));
assert!(bash.starts_with(" : 'NyaTermHistory:"));
assert!(bash.contains(":begin'; {\n"));
assert!(bash.contains("}; : 'NyaTermHistory:"));
assert!(bash.contains(":end'; __nyaterm_prune_history;"));
}
#[cfg(unix)]
#[test]
fn native_bash_injection_prunes_history_with_cmdhist_enabled_or_disabled() {
use std::io::Write;
use std::process::{Command, Stdio};
for cmdhist in ["shopt -s cmdhist", "shopt -u cmdhist"] {
let integration = injection_script(ShellKind::Bash, &build_ready_marker("session-1"))
.expect("Bash injection script");
let input = format!(
concat!(
"HISTFILE=/dev/null\n",
"HISTCONTROL=\n",
"history -c\n",
"{cmdhist}\n",
"PS1=\n",
"PS2=\n",
"{integration}",
"history\n",
"exit\n",
),
cmdhist = cmdhist,
integration = integration,
);
let mut child = Command::new("/bin/bash")
.args(["--noprofile", "--norc", "-i"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn interactive Bash");
child
.stdin
.take()
.expect("Bash stdin")
.write_all(input.as_bytes())
.expect("write Bash injection");
let output = child.wait_with_output().expect("wait for Bash");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "{cmdhist}\n{stdout}\n{stderr}");
assert!(
stdout.contains("NyaTermReady:session-1"),
"{cmdhist}\n{stdout}"
);
assert!(stdout.contains(cmdhist), "{cmdhist}\n{stdout}");
assert!(
!stdout.contains("NYATERM_PRUNE_HISTORY=1;")
&& !stdout.contains("__nyaterm_host(){")
&& !stdout.contains("NyaTermHistory:"),
"injection leaked into Bash history with {cmdhist}\n{stdout}"
);
}
}
#[test]