211 lines
6.2 KiB
Rust
211 lines
6.2 KiB
Rust
//! Job management commands (pause, resume, kill, clean).
|
|
|
|
use anyhow::{Context, Result};
|
|
use paracas_daemon::{DaemonSpawner, DownloadJob, JobStatus, StateManager};
|
|
|
|
/// Pause a running job by sending SIGSTOP to its process.
|
|
pub(crate) fn pause_job(state: &StateManager, job_id: &str) -> Result<()> {
|
|
let id = job_id.parse().context("Invalid job ID format")?;
|
|
|
|
let mut job: DownloadJob = state.load_job(id).context("Job not found")?;
|
|
|
|
if job.status != JobStatus::Running {
|
|
anyhow::bail!("Job is not running (status: {})", job.status);
|
|
}
|
|
|
|
let Some(pid) = job.pid else {
|
|
anyhow::bail!("Job has no associated process");
|
|
};
|
|
|
|
// Send SIGSTOP to pause the process
|
|
#[cfg(unix)]
|
|
{
|
|
use std::process::Command;
|
|
let status = Command::new("kill")
|
|
.args(["-STOP", &pid.to_string()])
|
|
.status()
|
|
.context("Failed to send SIGSTOP")?;
|
|
|
|
if !status.success() {
|
|
anyhow::bail!("Failed to pause process {}", pid);
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
// Windows doesn't have SIGSTOP equivalent, we'll just update the state
|
|
eprintln!(
|
|
"Warning: Pause is not fully supported on Windows. Job state updated but process continues."
|
|
);
|
|
}
|
|
|
|
job.mark_paused();
|
|
state.save_job(&job)?;
|
|
|
|
println!("Job {} paused.", id);
|
|
Ok(())
|
|
}
|
|
|
|
/// Resume a paused job by sending SIGCONT to its process.
|
|
pub(crate) fn resume_job(state: &StateManager, job_id: &str) -> Result<()> {
|
|
let id = job_id.parse().context("Invalid job ID format")?;
|
|
|
|
let mut job: DownloadJob = state.load_job(id).context("Job not found")?;
|
|
|
|
if job.status != JobStatus::Paused {
|
|
anyhow::bail!("Job is not paused (status: {})", job.status);
|
|
}
|
|
|
|
let Some(pid) = job.pid else {
|
|
anyhow::bail!("Job has no associated process");
|
|
};
|
|
|
|
// Check if the process is still alive
|
|
if !StateManager::is_process_running(pid) {
|
|
// Process is dead, need to respawn
|
|
println!("Process {} is no longer running. Respawning daemon...", pid);
|
|
return respawn_job(state, &mut job);
|
|
}
|
|
|
|
// Send SIGCONT to resume the process
|
|
#[cfg(unix)]
|
|
{
|
|
use std::process::Command;
|
|
let status = Command::new("kill")
|
|
.args(["-CONT", &pid.to_string()])
|
|
.status()
|
|
.context("Failed to send SIGCONT")?;
|
|
|
|
if !status.success() {
|
|
anyhow::bail!("Failed to resume process {}", pid);
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
eprintln!("Warning: Resume is not fully supported on Windows.");
|
|
}
|
|
|
|
job.mark_resumed(pid);
|
|
state.save_job(&job)?;
|
|
|
|
println!("Job {} resumed.", id);
|
|
Ok(())
|
|
}
|
|
|
|
/// Respawn a job that needs to be resumed but whose process is dead.
|
|
fn respawn_job(state: &StateManager, job: &mut DownloadJob) -> Result<()> {
|
|
let spawner = DaemonSpawner::new(state.clone()).context("Failed to create daemon spawner")?;
|
|
|
|
// Reset job status to pending so it can be picked up
|
|
job.status = JobStatus::Pending;
|
|
job.pid = None;
|
|
|
|
spawner.spawn(job).context("Failed to respawn daemon")?;
|
|
|
|
println!("Job {} respawned with PID {:?}.", job.id, job.pid);
|
|
Ok(())
|
|
}
|
|
|
|
/// Kill a running or paused job by sending SIGKILL to its process.
|
|
pub(crate) fn kill_job(state: &StateManager, job_id: &str) -> Result<()> {
|
|
let id = job_id.parse().context("Invalid job ID format")?;
|
|
|
|
let mut job: DownloadJob = state.load_job(id).context("Job not found")?;
|
|
|
|
if !matches!(
|
|
job.status,
|
|
JobStatus::Running | JobStatus::Pending | JobStatus::Paused
|
|
) {
|
|
anyhow::bail!("Job is not active (status: {})", job.status);
|
|
}
|
|
|
|
// Send SIGKILL to the process if it exists
|
|
if let Some(pid) = job.pid {
|
|
#[cfg(unix)]
|
|
{
|
|
use std::process::Command;
|
|
// First try SIGTERM for graceful shutdown
|
|
let _ = Command::new("kill")
|
|
.args(["-TERM", &pid.to_string()])
|
|
.status();
|
|
|
|
// Wait briefly then force kill if still running
|
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
|
|
if StateManager::is_process_running(pid) {
|
|
let _ = Command::new("kill")
|
|
.args(["-KILL", &pid.to_string()])
|
|
.status();
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
use std::process::Command;
|
|
let _ = Command::new("taskkill")
|
|
.args(["/F", "/PID", &pid.to_string()])
|
|
.status();
|
|
}
|
|
}
|
|
|
|
job.mark_cancelled();
|
|
state.save_job(&job)?;
|
|
|
|
println!("Job {} killed.", id);
|
|
Ok(())
|
|
}
|
|
|
|
/// Clean up completed, failed, or cancelled jobs from storage.
|
|
pub(crate) fn clean_jobs(state: &StateManager, all: bool) -> Result<()> {
|
|
let jobs = state.list_jobs()?;
|
|
|
|
let mut cleaned_count = 0;
|
|
|
|
for job in jobs {
|
|
let should_clean = if all {
|
|
job.is_finished()
|
|
} else {
|
|
// By default, only clean jobs older than 24 hours that are finished
|
|
let is_old = job.created_at < chrono::Utc::now() - chrono::Duration::hours(24);
|
|
is_old && job.is_finished()
|
|
};
|
|
|
|
if should_clean {
|
|
state.delete_job(job.id)?;
|
|
cleaned_count += 1;
|
|
}
|
|
}
|
|
|
|
if cleaned_count == 0 {
|
|
println!("No jobs to clean.");
|
|
} else {
|
|
println!("Cleaned {} job(s).", cleaned_count);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Execute the job management command.
|
|
pub(crate) fn job_command(action: &str, job_id: Option<&str>, all: bool) -> Result<()> {
|
|
let state_manager =
|
|
StateManager::with_default_path().context("Failed to initialize state manager")?;
|
|
|
|
match action {
|
|
"pause" => {
|
|
let id = job_id.context("Job ID required for pause")?;
|
|
pause_job(&state_manager, id)
|
|
}
|
|
"resume" => {
|
|
let id = job_id.context("Job ID required for resume")?;
|
|
resume_job(&state_manager, id)
|
|
}
|
|
"kill" => {
|
|
let id = job_id.context("Job ID required for kill")?;
|
|
kill_job(&state_manager, id)
|
|
}
|
|
"clean" => clean_jobs(&state_manager, all),
|
|
_ => anyhow::bail!("Unknown action: {}", action),
|
|
}
|
|
}
|