Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions rustatio-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ pub enum Commands {
#[arg(long, default_value = "10.0", value_name = "PERCENT")]
random_ratio_range: f64,

/// Action to take when stop conditions are met
#[arg(long, value_enum, default_value = "idle")]
post_stop_action: PostStopActionArg,

/// Enable progressive rate adjustment
#[arg(long)]
progressive: bool,
Expand Down Expand Up @@ -261,6 +265,26 @@ impl From<ClientArg> for rustatio_core::ClientType {
}
}

#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum PostStopActionArg {
/// Stay connected but stop uploading/downloading (default)
Idle,
/// Send a stop event to the tracker
StopSeeding,
/// Delete the instance entirely
DeleteInstance,
}

impl From<PostStopActionArg> for rustatio_core::PostStopAction {
fn from(action: PostStopActionArg) -> Self {
match action {
PostStopActionArg::Idle => Self::Idle,
PostStopActionArg::StopSeeding => Self::StopSeeding,
PostStopActionArg::DeleteInstance => Self::DeleteInstance,
}
}
}

#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ShellArg {
Bash,
Expand Down
3 changes: 3 additions & 0 deletions rustatio-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ async fn main() -> Result<()> {
random_range,
randomize_ratio,
random_ratio_range,
post_stop_action,
progressive,
target_upload,
target_download,
Expand Down Expand Up @@ -132,6 +133,7 @@ async fn main() -> Result<()> {
target_upload,
target_download,
progressive_duration,
post_stop_action,
json_mode: json,
stats_interval: interval,
save_session: save_session && !no_save_session,
Expand Down Expand Up @@ -225,6 +227,7 @@ async fn main() -> Result<()> {
stop_time: None,
idle_when_no_leechers: false,
idle_when_no_seeders: false,
post_stop_action: cli::PostStopActionArg::Idle,
no_randomize: false,
random_range: 20.0,
randomize_ratio: false,
Expand Down
2 changes: 2 additions & 0 deletions rustatio-cli/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct RunnerConfig {
pub stop_time: Option<f64>,
pub idle_when_no_leechers: bool,
pub idle_when_no_seeders: bool,
pub post_stop_action: crate::cli::PostStopActionArg,
pub no_randomize: bool,
pub random_range: f64,
pub randomize_ratio: bool,
Expand Down Expand Up @@ -298,6 +299,7 @@ pub fn create_faker_config(config: &RunnerConfig) -> FakerConfig {
idle_when_no_leechers: config.idle_when_no_leechers,
idle_when_no_seeders: config.idle_when_no_seeders,
scrape_interval: 60,
post_stop_action: config.post_stop_action.into(),
progressive_rates: config.progressive,
target_upload_rate: config.target_upload,
target_download_rate: config.target_download,
Expand Down
3 changes: 3 additions & 0 deletions rustatio-core/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::faker::PostStopAction;
use crate::torrent::ClientType;
use serde::{Deserialize, Serialize};
use std::fs;
Expand Down Expand Up @@ -72,6 +73,8 @@ pub struct InstanceConfig {
pub stop_at_seed_time_hours: f64,
pub idle_when_no_leechers: bool,
pub idle_when_no_seeders: bool,
#[serde(default)]
pub post_stop_action: PostStopAction,
pub progressive_rates_enabled: bool,
pub target_upload_rate: f64,
pub target_download_rate: f64,
Expand Down
151 changes: 115 additions & 36 deletions rustatio-core/src/faker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ pub struct FakerConfig {
/// Time in seconds to reach target rates
#[serde(default = "default_progressive_duration")]
pub progressive_duration: u64,

/// What to do when stop conditions are met
#[serde(default)]
pub post_stop_action: PostStopAction,
}

/// UI-friendly preset settings format (matches frontend)
Expand Down Expand Up @@ -149,6 +153,7 @@ pub struct PresetSettings {
pub stop_at_seed_time_hours: Option<f64>,
pub idle_when_no_leechers: Option<bool>,
pub idle_when_no_seeders: Option<bool>,
pub post_stop_action: Option<String>,
// Progressive rates
pub progressive_rates_enabled: Option<bool>,
pub target_upload_rate: Option<f64>,
Expand Down Expand Up @@ -202,6 +207,11 @@ impl From<PresetSettings> for FakerConfig {
idle_when_no_leechers: p.idle_when_no_leechers.unwrap_or(false),
idle_when_no_seeders: p.idle_when_no_seeders.unwrap_or(false),
scrape_interval: 60,
post_stop_action: match p.post_stop_action.as_deref() {
Some("stop_seeding") => PostStopAction::StopSeeding,
Some("delete_instance") => PostStopAction::DeleteInstance,
_ => PostStopAction::Idle,
},
progressive_rates: p.progressive_rates_enabled.unwrap_or(false),
target_upload_rate: p.target_upload_rate,
target_download_rate: p.target_download_rate,
Expand Down Expand Up @@ -259,10 +269,20 @@ impl Default for FakerConfig {
target_upload_rate: None,
target_download_rate: None,
progressive_duration: 3600,
post_stop_action: PostStopAction::Idle,
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PostStopAction {
#[default]
Idle,
StopSeeding,
DeleteInstance,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FakerState {
Idle,
Expand Down Expand Up @@ -324,6 +344,12 @@ pub struct FakerStats {
pub ratio_history: Vec<f64>,
pub history_timestamps: Vec<u64>, // Unix timestamps in milliseconds

// === STOP CONDITION STATE ===
#[serde(default)]
pub stop_condition_met: bool,
#[serde(default)]
pub post_stop_action: PostStopAction,

// === INTERNAL ===
#[serde(skip)]
pub last_announce: Option<Instant>,
Expand Down Expand Up @@ -539,6 +565,9 @@ impl RatioFaker {
last_announce: None,
next_announce: None,
announce_count: 0,

stop_condition_met: false,
post_stop_action: config.post_stop_action,
};

Ok(Self {
Expand Down Expand Up @@ -656,6 +685,24 @@ impl RatioFaker {
self.stats.idling_reason = None;
}

async fn apply_post_stop_action(&mut self) -> Result<()> {
self.stats.stop_condition_met = true;
match self.config.post_stop_action {
PostStopAction::Idle => {
log_info!("Stop condition met, idling (post_stop_action=idle)");
self.stats.is_idling = true;
self.stats.idling_reason = Some("stop_condition_met".to_string());
self.stats.current_upload_rate = 0.0;
self.stats.current_download_rate = 0.0;
}
PostStopAction::StopSeeding | PostStopAction::DeleteInstance => {
log_info!("Stop condition met, stopping faker");
self.stop().await?;
}
}
Ok(())
}

/// Update the fake stats (call this periodically)
pub async fn update(&mut self) -> Result<()> {
let now = Instant::now();
Expand Down Expand Up @@ -692,8 +739,7 @@ impl RatioFaker {
}

if outcome.stop {
log_info!("Stop condition met, stopping faker");
self.stop().await?;
self.apply_post_stop_action().await?;
}

Ok(())
Expand All @@ -707,9 +753,20 @@ impl RatioFaker {
let (base_upload_rate, base_download_rate) = self.calc_base_rates(&inputs);
let (upload_rate, download_rate) =
self.apply_randomized_rates(base_upload_rate, base_download_rate, inputs.left);
let (upload_rate, download_rate, is_idling, idling_reason) =
let (mut upload_rate, mut download_rate, is_idling, idling_reason) =
Self::apply_idling_rules(&inputs, upload_rate, download_rate);

// Preserve idling state if stop condition was met with post_stop_action=Idle
let (is_idling, idling_reason) = if self.stats.stop_condition_met
&& self.config.post_stop_action == PostStopAction::Idle
{
upload_rate = 0.0;
download_rate = 0.0;
(true, Some("stop_condition_met".to_string()))
} else {
(is_idling, idling_reason)
};

self.stats.is_idling = is_idling;
self.stats.idling_reason = idling_reason;

Expand Down Expand Up @@ -972,8 +1029,7 @@ impl RatioFaker {
}

if outcome.stop {
log_info!("Stop condition met, stopping faker");
self.stop().await?;
self.apply_post_stop_action().await?;
}

Ok(())
Expand Down Expand Up @@ -1025,6 +1081,8 @@ impl RatioFaker {
last_announce: None,
next_announce: None,
announce_count: 0,
stop_condition_met: false,
post_stop_action: config.post_stop_action,
}
}

Expand Down Expand Up @@ -1201,7 +1259,7 @@ impl RatioFaker {
stats.ratio = current_ratio;
Self::add_to_history(&mut stats.ratio_history, current_ratio, 60);

// Session ratio (for stop conditions) = session_uploaded / torrent_size
// Session ratio = session_uploaded / torrent_size
stats.session_ratio = if torrent_size > 0 {
stats.session_uploaded as f64 / torrent_size as f64
} else {
Expand Down Expand Up @@ -1256,36 +1314,41 @@ impl RatioFaker {
}

fn check_stop_conditions(&self, stats: &FakerStats) -> bool {
// Check ratio target (use session ratio, not cumulative)
// Don't re-trigger if already met
if stats.stop_condition_met {
return false;
}

// Check ratio target (cumulative across all sessions)
if let Some(target_ratio) = self.config.stop_at_ratio {
if stats.session_ratio >= target_ratio - 0.001 {
if stats.ratio >= target_ratio - 0.001 {
log_info!(
"Target ratio reached: {:.3} >= {:.3} (session)",
stats.session_ratio,
"Target ratio reached: {:.3} >= {:.3} (cumulative)",
stats.ratio,
target_ratio
);
return true;
}
}

// Check uploaded target (session uploaded, not total)
// Check uploaded target (cumulative across all sessions)
if let Some(target_uploaded) = self.config.stop_at_uploaded {
if stats.session_uploaded >= target_uploaded {
if stats.uploaded >= target_uploaded {
log_info!(
"Target uploaded reached: {} >= {} bytes (session)",
stats.session_uploaded,
"Target uploaded reached: {} >= {} bytes (cumulative)",
stats.uploaded,
target_uploaded
);
return true;
}
}

// Check downloaded target (session downloaded, not total)
// Check downloaded target (cumulative across all sessions)
if let Some(target_downloaded) = self.config.stop_at_downloaded {
if stats.session_downloaded >= target_downloaded {
if stats.downloaded >= target_downloaded {
log_info!(
"Target downloaded reached: {} >= {} bytes (session)",
stats.session_downloaded,
"Target downloaded reached: {} >= {} bytes (cumulative)",
stats.downloaded,
target_downloaded
);
return true;
Expand Down Expand Up @@ -1456,6 +1519,38 @@ impl RatioFakerHandle {
result
}

async fn apply_post_stop_action(&self) -> Result<()> {
let post_stop_action = {
let guard = self.inner.lock().await;
guard.config.post_stop_action
};
match post_stop_action {
PostStopAction::Idle => {
log_info!("Stop condition met, idling (post_stop_action=idle)");
let mut guard = self.inner.lock().await;
guard.stats.stop_condition_met = true;
guard.stats.is_idling = true;
guard.stats.idling_reason = Some("stop_condition_met".to_string());
guard.stats.current_upload_rate = 0.0;
guard.stats.current_download_rate = 0.0;
}
PostStopAction::StopSeeding | PostStopAction::DeleteInstance => {
log_info!("Stop condition met, stopping faker");
let plan = {
let mut guard = self.inner.lock().await;
guard.stats.stop_condition_met = true;
guard.begin_stop()
};
if let Some(plan) = plan {
let result = plan.execute().await;
let mut guard = self.inner.lock().await;
guard.apply_stop_result(result);
}
}
}
Ok(())
}

pub async fn update(&self) -> Result<()> {
let now = Instant::now();
let outcome = {
Expand Down Expand Up @@ -1501,15 +1596,7 @@ impl RatioFakerHandle {
}

if outcome.stop {
let plan = {
let mut guard = self.inner.lock().await;
guard.begin_stop()
};
if let Some(plan) = plan {
let result = plan.execute().await;
let mut guard = self.inner.lock().await;
guard.apply_stop_result(result);
}
self.apply_post_stop_action().await?;
}

let guard = self.inner.lock().await;
Expand Down Expand Up @@ -1552,15 +1639,7 @@ impl RatioFakerHandle {
}

if outcome.stop {
let plan = {
let mut guard = self.inner.lock().await;
guard.begin_stop()
};
if let Some(plan) = plan {
let result = plan.execute().await;
let mut guard = self.inner.lock().await;
guard.apply_stop_result(result);
}
self.apply_post_stop_action().await?;
}

let guard = self.inner.lock().await;
Expand Down
4 changes: 3 additions & 1 deletion rustatio-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ pub mod validation;
pub use config::{AppConfig, ClientSettings, ConfigError, FakerSettings, UiSettings};
#[cfg(not(target_arch = "wasm32"))]
pub use faker::RatioFakerHandle;
pub use faker::{FakerConfig, FakerError, FakerState, FakerStats, PresetSettings, RatioFaker};
pub use faker::{
FakerConfig, FakerError, FakerState, FakerStats, PostStopAction, PresetSettings, RatioFaker,
};
pub use grid::{GridImportSettings, GridMode, InstanceSummary};
pub use torrent::{
ClientConfig, ClientInfo, ClientType, HttpVersion, TorrentError, TorrentFile, TorrentInfo,
Expand Down
Loading
Loading