mirror of
https://github.com/nyakang/nyaterm.git
synced 2026-09-22 00:01:30 +00:00
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:
+118
-19
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user