#![cfg(feature = "daemon")]
use running_process::daemon::client::DaemonClient;
use running_process::daemon::paths;
use running_process::daemon::pty_session::{PtyAttachment, PtySpawnRequest};
use running_process::daemon::server::DaemonServer;
use running_process::proto::daemon::pty_stream_frame::Frame as StreamOneof;
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant};
fn testbin_path(name: &str) -> PathBuf {
let output = Command::new(env!("CARGO"))
.args([
"build",
"-p",
"testbins",
"--bin",
name,
"--message-format=json",
])
.stderr(std::process::Stdio::inherit())
.output()
.expect("cargo build for testbin failed");
assert!(output.status.success(), "cargo build -p {name} failed");
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if !line.contains("\"compiler-artifact\"") || !line.contains(name) {
continue;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
if v["reason"] == "compiler-artifact"
&& v["target"]["kind"]
.as_array()
.is_some_and(|a| a.iter().any(|k| k == "bin"))
{
if let Some(exe) = v["executable"].as_str() {
let p = PathBuf::from(exe);
let deadline = Instant::now() + Duration::from_secs(5);
while !p.exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(50));
}
return p;
}
}
}
}
panic!("could not find binary artifact for {name}");
}
fn start_server(scope: &str) -> (tokio::task::JoinHandle<()>, String) {
let socket = paths::socket_path(Some(scope));
let db = paths::db_path(Some(scope)).to_string_lossy().into_owned();
let server = DaemonServer::new(
socket.clone(),
db,
"tui-repaint-test".to_string(),
scope.to_string(),
std::env::current_dir()
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
)
.expect("DaemonServer::new");
let handle = tokio::spawn(async move {
server.run().await.expect("server.run");
});
(handle, socket)
}
fn drain_attachment(att: &mut PtyAttachment, deadline: Instant) -> Vec<u8> {
let mut out = att.initial_backlog.clone();
while Instant::now() < deadline {
let remaining = deadline.saturating_duration_since(Instant::now());
match att.recv_frame_with_timeout(remaining) {
Ok(Some(frame)) => match frame.frame {
Some(StreamOneof::Output(bytes)) => out.extend_from_slice(&bytes),
Some(_) => break,
None => continue,
},
Ok(None) => break,
Err(_) => break,
}
}
out
}
#[cfg(windows)]
fn windows_build_number() -> Option<u32> {
use std::process::Command;
let output = Command::new("cmd").args(["/c", "ver"]).output().ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let v = stdout.split('.').nth(2)?;
v.parse::<u32>().ok()
}
#[cfg(not(windows))]
fn windows_build_number() -> Option<u32> {
None
}
#[cfg(windows)]
fn sidecar_active_on_windows() -> bool {
running_process::pty::current_backend_kind() == running_process::pty::ConPtyBackendKind::Sidecar
}
#[cfg(not(windows))]
#[allow(dead_code)]
fn sidecar_active_on_windows() -> bool {
false
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn raw_ansi_bytes_flow_through_pty_to_ring_buffer() {
if let Some(build) = windows_build_number() {
if build < 22000 && !sidecar_active_on_windows() {
eprintln!(
"SKIPPED: PSEUDOCONSOLE_PASSTHROUGH_MODE requires Windows 11+ \
(current build {build}) OR a sidecar conpty.dll loaded by \
the running-process backend. The transparent self-acquire \
path (#445) writes the sidecar to the platform cache on \
first use when GitHub release assets are available; for \
manual staging place `conpty.dll` next to the test exe — \
see WINDOWS_CONPTY_VERSION.txt and #443."
);
return;
}
}
let scope = format!("tui-repaint-{}", line!());
let (_handle, socket) = start_server(&scope);
tokio::time::sleep(Duration::from_millis(300)).await;
let tui_counter = tokio::task::spawn_blocking(|| testbin_path("testbin-tui-counter"))
.await
.expect("testbin");
let socket_for_test = socket.clone();
tokio::task::spawn_blocking(move || {
let mut control = DaemonClient::connect_to(&socket_for_test).expect("connect");
let argv = vec![tui_counter.to_string_lossy().into_owned()];
let spawn_req = PtySpawnRequest::new(argv)
.with_size(24, 80)
.with_originator("tui-repaint-test");
let spawned = control
.spawn_pty_session(&spawn_req)
.expect("spawn_pty_session");
assert!(spawned.pid > 0);
let mut att =
PtyAttachment::attach_to(&socket_for_test, &spawned.session_id, 30, 100, false)
.expect("attach");
let deadline = Instant::now() + Duration::from_secs(3);
let bytes = drain_attachment(&mut att, deadline);
assert!(
!bytes.is_empty(),
"expected non-empty backlog from testbin output over 3s"
);
assert!(
bytes.windows(4).any(|w| w == b"\x1b[2J"),
"clear-screen escape `\\x1b[2J` missing from backlog: {:?}",
String::from_utf8_lossy(&bytes)
);
assert!(
bytes.windows(6).any(|w| w == b"\x1b[1;1H"),
"cursor-home escape `\\x1b[1;1H` missing from backlog: {:?}",
String::from_utf8_lossy(&bytes)
);
let text = String::from_utf8_lossy(&bytes).into_owned();
assert!(
text.contains("COUNTER: 0"),
"first counter line missing from backlog: {text}"
);
assert!(
text.contains("COUNTER: 9"),
"last counter line missing from backlog: {text}"
);
let _ = control.shutdown(true, 5.0);
})
.await
.expect("client task");
}