use tokio::sync::mpsc;
use crate::install::{ExecutorOutput, ExecutorRequest};
#[cfg(not(target_os = "windows"))]
use crate::install::{
build_downgrade_command_for_executor, build_install_command_for_executor,
build_remove_command_for_executor, build_scan_command_for_executor,
build_update_command_for_executor,
};
#[cfg(not(target_os = "windows"))]
#[allow(clippy::needless_pass_by_value)] fn handle_install_request(
items: Vec<crate::state::PackageItem>,
password: Option<crate::state::SecureString>,
dry_run: bool,
res_tx: mpsc::UnboundedSender<ExecutorOutput>,
) {
use crate::state::Source;
tracing::info!(
"[Runtime] Executor worker received install request: {} items, dry_run={}",
items.len(),
dry_run
);
let has_aur = items.iter().any(|item| matches!(item.source, Source::Aur));
let cmd = match if has_aur {
build_install_command_for_executor(&items, None, dry_run)
} else {
build_install_command_for_executor(&items, password.as_deref(), dry_run)
} {
Ok(c) => c,
Err(err) => {
let _ = res_tx.send(ExecutorOutput::Error(err));
return;
}
};
let final_cmd = if has_aur && !dry_run && password.is_some() {
if let Some(ref pass) = password {
match crate::logic::privilege::active_tool() {
Ok(tool) => {
if let Some(warmup) =
crate::logic::privilege::build_credential_warmup(tool, pass)
{
format!("{warmup} ; {cmd}")
} else {
cmd
}
}
Err(err) => {
let _ = res_tx.send(ExecutorOutput::Error(err));
return;
}
}
} else {
cmd
}
} else {
cmd
};
let res_tx_clone = res_tx;
tokio::task::spawn_blocking(move || {
execute_command_pty(&final_cmd, None, res_tx_clone);
});
}
#[cfg(not(target_os = "windows"))]
#[allow(clippy::needless_pass_by_value)] fn handle_remove_request(
names: Vec<String>,
password: Option<crate::state::SecureString>,
cascade: crate::state::modal::CascadeMode,
dry_run: bool,
res_tx: mpsc::UnboundedSender<ExecutorOutput>,
) {
tracing::info!(
"[Runtime] Executor worker received remove request: {} packages, dry_run={}",
names.len(),
dry_run
);
let cmd = match build_remove_command_for_executor(&names, password.as_deref(), cascade, dry_run)
{
Ok(c) => c,
Err(err) => {
let _ = res_tx.send(ExecutorOutput::Error(err));
return;
}
};
let res_tx_clone = res_tx;
tokio::task::spawn_blocking(move || {
execute_command_pty(&cmd, None, res_tx_clone);
});
}
#[cfg(not(target_os = "windows"))]
#[allow(clippy::needless_pass_by_value)] fn handle_downgrade_request(
names: Vec<String>,
password: Option<crate::state::SecureString>,
dry_run: bool,
res_tx: mpsc::UnboundedSender<ExecutorOutput>,
) {
tracing::info!(
"[Runtime] Executor worker received downgrade request: {} packages, dry_run={}",
names.len(),
dry_run
);
let cmd = match build_downgrade_command_for_executor(&names, password.as_deref(), dry_run) {
Ok(c) => c,
Err(err) => {
let _ = res_tx.send(ExecutorOutput::Error(err));
return;
}
};
let res_tx_clone = res_tx;
tokio::task::spawn_blocking(move || {
execute_command_pty(&cmd, None, res_tx_clone);
});
}
#[cfg(not(target_os = "windows"))]
#[allow(clippy::needless_pass_by_value)] fn handle_update_request(
commands: Vec<String>,
password: Option<crate::state::SecureString>,
dry_run: bool,
res_tx: mpsc::UnboundedSender<ExecutorOutput>,
) {
tracing::info!(
"[Runtime] Executor worker received update request: {} commands, dry_run={}",
commands.len(),
dry_run
);
let cmd = match build_update_command_for_executor(&commands, password.as_deref(), dry_run) {
Ok(c) => c,
Err(err) => {
let _ = res_tx.send(ExecutorOutput::Error(err));
return;
}
};
tracing::debug!("[Runtime] Built update command (length={})", cmd.len());
let res_tx_clone = res_tx;
tokio::task::spawn_blocking(move || {
tracing::debug!("[Runtime] spawn_blocking started for update command");
execute_command_pty(&cmd, None, res_tx_clone);
tracing::debug!("[Runtime] spawn_blocking completed for update command");
});
}
#[cfg(not(target_os = "windows"))]
#[allow(
clippy::needless_pass_by_value, // Values are moved into spawn_blocking closure
clippy::too_many_arguments, // Scan configuration requires multiple flags
clippy::fn_params_excessive_bools // Scan configuration requires multiple bool flags
)]
fn handle_scan_request(
package: String,
do_clamav: bool,
do_trivy: bool,
do_semgrep: bool,
do_shellcheck: bool,
do_virustotal: bool,
do_custom: bool,
dry_run: bool,
res_tx: mpsc::UnboundedSender<ExecutorOutput>,
) {
tracing::info!(
"[Runtime] Executor worker received scan request: package={}, dry_run={}",
package,
dry_run
);
let cmd = build_scan_command_for_executor(
&package,
do_clamav,
do_trivy,
do_semgrep,
do_shellcheck,
do_virustotal,
do_custom,
dry_run,
);
let res_tx_clone = res_tx;
tokio::task::spawn_blocking(move || {
execute_command_pty(&cmd, None, res_tx_clone);
});
}
#[cfg(not(target_os = "windows"))]
#[allow(clippy::needless_pass_by_value)] fn handle_custom_command_request(
command: String,
password: Option<crate::state::SecureString>,
dry_run: bool,
res_tx: mpsc::UnboundedSender<ExecutorOutput>,
) {
tracing::info!(
"[Runtime] Executor worker received custom command request, dry_run={}",
dry_run
);
let cmd = if dry_run {
use crate::install::shell_single_quote;
let quoted = shell_single_quote(&command);
format!("echo DRY RUN: {quoted}")
} else {
let tool = match crate::logic::privilege::active_tool() {
Ok(t) => t,
Err(err) => {
let _ = res_tx.send(ExecutorOutput::Error(err));
return;
}
};
let needs_askpass = command.contains(tool.binary_name())
&& tool.capabilities().supports_askpass
&& password.is_some();
if needs_askpass {
if let Some(ref pass) = password {
use std::fs;
use std::os::unix::fs::PermissionsExt;
let temp_dir = std::env::temp_dir();
#[allow(clippy::uninlined_format_args)]
let askpass_script =
temp_dir.join(format!("pacsea_sudo_askpass_{}.sh", std::process::id()));
let escaped_pass = pass.replace('\'', "'\\''");
#[allow(clippy::uninlined_format_args)] let script_content = format!("#!/bin/sh\nprintf '%s\\n' '{}'\n", escaped_pass);
if let Err(e) = fs::write(&askpass_script, script_content) {
let _ = res_tx.send(ExecutorOutput::Error(format!(
"Failed to create sudo askpass script: {e}"
)));
return;
}
if let Err(e) =
fs::set_permissions(&askpass_script, fs::Permissions::from_mode(0o755))
{
let _ = res_tx.send(ExecutorOutput::Error(format!(
"Failed to make askpass script executable: {e}"
)));
return;
}
let askpass_path = askpass_script.to_string_lossy().to_string();
let escaped_path = askpass_path.replace('\'', "'\\''");
let final_cmd = format!(
"export SUDO_ASKPASS='{escaped_path}'; {command}; rm -f '{escaped_path}'"
);
final_cmd
} else {
command
}
} else {
command
}
};
let res_tx_clone = res_tx;
tokio::task::spawn_blocking(move || {
execute_command_pty(&cmd, password, res_tx_clone);
});
}
#[cfg(not(target_os = "windows"))]
pub fn spawn_executor_worker(
executor_req_rx: mpsc::UnboundedReceiver<ExecutorRequest>,
executor_res_tx: mpsc::UnboundedSender<ExecutorOutput>,
) {
let executor_res_tx_bg = executor_res_tx;
tokio::spawn(async move {
let mut executor_req_rx = executor_req_rx;
tracing::info!("[Runtime] Executor worker started, waiting for requests...");
while let Some(request) = executor_req_rx.recv().await {
let res_tx = executor_res_tx_bg.clone();
match request {
ExecutorRequest::Install {
items,
password,
dry_run,
} => handle_install_request(items, password, dry_run, res_tx),
ExecutorRequest::Remove {
names,
password,
cascade,
dry_run,
} => handle_remove_request(names, password, cascade, dry_run, res_tx),
ExecutorRequest::Downgrade {
names,
password,
dry_run,
} => handle_downgrade_request(names, password, dry_run, res_tx),
ExecutorRequest::Update {
commands,
password,
dry_run,
} => handle_update_request(commands, password, dry_run, res_tx),
#[cfg(not(target_os = "windows"))]
ExecutorRequest::Scan {
package,
do_clamav,
do_trivy,
do_semgrep,
do_shellcheck,
do_virustotal,
do_custom,
dry_run,
} => handle_scan_request(
package,
do_clamav,
do_trivy,
do_semgrep,
do_shellcheck,
do_virustotal,
do_custom,
dry_run,
res_tx,
),
ExecutorRequest::CustomCommand {
command,
password,
dry_run,
} => handle_custom_command_request(command, password, dry_run, res_tx),
}
}
tracing::debug!("[Runtime] Executor worker exiting (channel closed)");
});
}
#[cfg(not(target_os = "windows"))]
fn process_text_chars(
text: &str,
line_buffer: &mut String,
res_tx: &mpsc::UnboundedSender<ExecutorOutput>,
) -> usize {
let mut lines_sent = 0;
for ch in text.chars() {
match ch {
'\n' => {
if !line_buffer.trim().is_empty() {
let cleaned = strip_ansi_escapes::strip_str(&*line_buffer);
tracing::trace!(
"[PTY] Sending line: {}...",
&cleaned[..cleaned.len().min(50)]
);
if res_tx.send(ExecutorOutput::Line(cleaned)).is_ok() {
lines_sent += 1;
} else {
tracing::warn!("[PTY] Failed to send line - channel closed");
}
}
line_buffer.clear();
}
'\r' => {
if !line_buffer.trim().is_empty() {
let cleaned = strip_ansi_escapes::strip_str(&*line_buffer);
let has_progress_brackets = cleaned.contains('[') && cleaned.contains(']');
let has_percentage = cleaned.contains('%');
let looks_like_progress = has_progress_brackets && has_percentage;
if looks_like_progress {
if res_tx
.send(ExecutorOutput::ReplaceLastLine(cleaned))
.is_ok()
{
lines_sent += 1;
}
} else {
if res_tx.send(ExecutorOutput::Line(cleaned)).is_ok() {
lines_sent += 1;
}
}
}
line_buffer.clear();
}
_ => {
line_buffer.push(ch);
}
}
}
lines_sent
}
#[cfg(not(target_os = "windows"))]
#[cfg(not(target_os = "windows"))]
fn spawn_pty_reader_thread(
reader: Box<dyn std::io::Read + Send>,
data_tx: std::sync::mpsc::Sender<Vec<u8>>,
) {
std::thread::spawn(move || {
tracing::debug!("[PTY Reader] Reader thread started");
let mut reader = reader;
let mut total_bytes_read: usize = 0;
loop {
let mut buf = [0u8; 4096];
match reader.read(&mut buf) {
Ok(0) => {
tracing::debug!(
"[PTY Reader] EOF received, total bytes read: {}",
total_bytes_read
);
let _ = data_tx.send(Vec::new());
break;
}
Ok(n) => {
total_bytes_read += n;
tracing::trace!(
"[PTY Reader] Read {} bytes (total: {})",
n,
total_bytes_read
);
if data_tx.send(buf[..n].to_vec()).is_err() {
tracing::debug!("[PTY Reader] Receiver dropped, exiting");
break;
}
}
Err(e) => {
tracing::debug!(
"[PTY Reader] Read error: {}, total bytes: {}",
e,
total_bytes_read
);
break;
}
}
}
tracing::debug!("[PTY Reader] Reader thread exiting");
});
}
#[cfg(not(target_os = "windows"))]
fn process_byte_buffer_utf8(
byte_buffer: &mut Vec<u8>,
line_buffer: &mut String,
res_tx: &mpsc::UnboundedSender<ExecutorOutput>,
) -> usize {
let mut lines_sent = 0;
loop {
if let Ok(text) = String::from_utf8(byte_buffer.clone()) {
byte_buffer.clear();
lines_sent += process_text_chars(&text, line_buffer, res_tx);
break;
}
if byte_buffer.len() < 4 {
break;
}
let mut found_valid = false;
for trim_len in 1..=4.min(byte_buffer.len()) {
let test_len = byte_buffer.len().saturating_sub(trim_len);
if test_len == 0 {
break;
}
if let Ok(text) = String::from_utf8(byte_buffer[..test_len].to_vec()) {
lines_sent += process_text_chars(&text, line_buffer, res_tx);
byte_buffer.drain(..test_len);
found_valid = true;
break;
}
}
if !found_valid {
let text = String::from_utf8_lossy(byte_buffer);
lines_sent += process_text_chars(&text, line_buffer, res_tx);
byte_buffer.clear();
break;
}
}
lines_sent
}
#[cfg(not(target_os = "windows"))]
fn send_finish_message(
line_buffer: &str,
lines_sent: &mut usize,
exit_code_u32: u32,
res_tx: &mpsc::UnboundedSender<ExecutorOutput>,
context: &str,
) {
if !line_buffer.trim().is_empty() {
let cleaned = strip_ansi_escapes::strip_str(line_buffer);
if res_tx.send(ExecutorOutput::Line(cleaned)).is_ok() {
*lines_sent += 1;
}
}
let success = exit_code_u32 == 0;
let exit_code = i32::try_from(exit_code_u32).ok();
tracing::info!(
"[PTY] Process finished{}: success={}, exit_code={:?}, total_lines_sent={}",
context,
success,
exit_code,
lines_sent
);
let _ = res_tx.send(ExecutorOutput::Finished {
success,
exit_code,
failed_command: None,
});
}
#[cfg(not(target_os = "windows"))]
#[allow(clippy::needless_pass_by_value)] fn execute_command_pty(
cmd: &str,
_password: Option<crate::state::SecureString>,
res_tx: mpsc::UnboundedSender<ExecutorOutput>,
) {
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use std::sync::mpsc as std_mpsc;
tracing::debug!("[PTY] Starting execute_command_pty");
tracing::debug!("[PTY] Command length: {} chars", cmd.len());
let pty_system = native_pty_system();
let pty_size = PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
};
tracing::debug!("[PTY] Opening PTY");
let pty = match pty_system.openpty(pty_size) {
Ok(pty) => pty,
Err(e) => {
tracing::error!("[PTY] Failed to open PTY: {e}");
let _ = res_tx.send(ExecutorOutput::Error(format!("Failed to open PTY: {e}")));
return;
}
};
let mut cmd_builder = CommandBuilder::new("bash");
cmd_builder.arg("-c");
cmd_builder.arg(cmd);
tracing::debug!("[PTY] Spawning bash command");
let mut child = match pty.slave.spawn_command(cmd_builder) {
Ok(child) => child,
Err(e) => {
tracing::error!("[PTY] Failed to spawn command: {e}");
let _ = res_tx.send(ExecutorOutput::Error(format!("Failed to spawn: {e}")));
return;
}
};
let reader = pty
.master
.try_clone_reader()
.expect("Failed to clone reader");
let _master = pty.master; let (data_tx, data_rx) = std_mpsc::channel::<Vec<u8>>();
spawn_pty_reader_thread(reader, data_tx);
let mut byte_buffer = Vec::new();
let mut line_buffer = String::new();
let mut lines_sent: usize = 0;
tracing::debug!("[PTY] Entering main processing loop");
loop {
match child.try_wait() {
Ok(Some(status)) => {
tracing::debug!("[PTY] Child exited with code: {:?}", status.exit_code());
drain_remaining_data(
&data_rx,
&mut byte_buffer,
&mut line_buffer,
&mut lines_sent,
&res_tx,
);
send_finish_message(
&line_buffer,
&mut lines_sent,
status.exit_code(),
&res_tx,
"",
);
return;
}
Ok(None) => {} Err(e) => {
tracing::error!("[PTY] Error checking child status: {e}");
let _ = res_tx.send(ExecutorOutput::Error(format!("Process error: {e}")));
return;
}
}
match data_rx.recv_timeout(std::time::Duration::from_millis(50)) {
Ok(data) if data.is_empty() => {
tracing::debug!("[PTY] Got EOF signal");
break;
}
Ok(data) => {
byte_buffer.extend_from_slice(&data);
lines_sent += process_byte_buffer_utf8(&mut byte_buffer, &mut line_buffer, &res_tx);
}
Err(std_mpsc::RecvTimeoutError::Timeout) => {}
Err(std_mpsc::RecvTimeoutError::Disconnected) => {
tracing::debug!("[PTY] Read thread disconnected");
break;
}
}
}
tracing::debug!("[PTY] Waiting for child process after EOF");
match child.wait() {
Ok(status) => {
send_finish_message(
&line_buffer,
&mut lines_sent,
status.exit_code(),
&res_tx,
" (post-loop)",
);
}
Err(e) => {
tracing::error!("[PTY] Child wait error: {e}");
let _ = res_tx.send(ExecutorOutput::Error(format!("Wait error: {e}")));
}
}
}
#[cfg(not(target_os = "windows"))]
fn drain_remaining_data(
data_rx: &std::sync::mpsc::Receiver<Vec<u8>>,
byte_buffer: &mut Vec<u8>,
line_buffer: &mut String,
lines_sent: &mut usize,
res_tx: &mpsc::UnboundedSender<ExecutorOutput>,
) {
while let Ok(data) = data_rx.recv_timeout(std::time::Duration::from_millis(100)) {
if data.is_empty() {
break;
}
byte_buffer.extend_from_slice(&data);
if let Ok(text) = String::from_utf8(byte_buffer.clone()) {
byte_buffer.clear();
*lines_sent += process_text_chars(&text, line_buffer, res_tx);
}
}
}
#[cfg(target_os = "windows")]
pub fn spawn_executor_worker(
mut executor_req_rx: mpsc::UnboundedReceiver<ExecutorRequest>,
executor_res_tx: mpsc::UnboundedSender<ExecutorOutput>,
) {
tokio::spawn(async move {
while let Some(_request) = executor_req_rx.recv().await {
let _ = executor_res_tx.send(ExecutorOutput::Error(
"PTY execution is not supported on Windows".to_string(),
));
}
});
}