Skip to content
Open
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
40 changes: 13 additions & 27 deletions smitebot/src/commands/corpus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,38 +70,20 @@ impl CorpusCommand {
}
}

/// Loads the campaign state for `campaign_id`, logging a not-found hint on error.
fn load_campaign(runs_dir: &Path, campaign_id: &str) -> Option<CampaignState> {
let state_path = runs_dir.join(campaign_id).join("state.json");
match CampaignState::load(&state_path) {
Ok(state) => Some(state),
Err(e) => {
log::error!("{e}");
log::error!(
"campaign '{campaign_id}' not found; list campaigns with: ls {}",
runs_dir.display()
);
None
}
}
}

/// Loads every campaign's state, then merges their runner queues into `output`.
///
/// All states are loaded before any file is written, so a bad campaign ID fails
/// before the output directory is touched rather than leaving a partial merge.
fn execute_merge(args: &MergeArgs) -> bool {
let Some(runs_dir) = CampaignState::runs_dir() else {
log::error!("unable to determine home directory");
return false;
};

let mut states = Vec::with_capacity(args.campaign_ids.len());
for campaign_id in &args.campaign_ids {
let Some(state) = load_campaign(&runs_dir, campaign_id) else {
return false;
};
states.push(state);
match CampaignState::load_campaign(campaign_id) {
Ok(state) => states.push(state),
Err(e) => {
log::error!("{e}");
return false;
}
}
}

if output_dir_occupied(&args.output) {
Expand Down Expand Up @@ -222,8 +204,12 @@ fn execute_minimize(args: &MinimizeArgs) -> bool {
return false;
};

let Some(state) = load_campaign(&runs_dir, &args.campaign_id) else {
return false;
let state = match CampaignState::load_campaign(&args.campaign_id) {
Ok(s) => s,
Err(e) => {
log::error!("{e}");
return false;
}
};

// The --aflpp-path flag overrides the path recorded at campaign start, for when
Expand Down
13 changes: 1 addition & 12 deletions smitebot/src/commands/reproduce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,10 @@ impl ReproduceCommand {
/// `false` only on an operational failure: unknown campaign, missing input,
/// missing image, or a Docker spawn error.
pub fn execute(args: &ReproduceArgs) -> bool {
let Some(runs_dir) = CampaignState::runs_dir() else {
log::error!("unable to determine home directory");
return false;
};

let state_path = runs_dir.join(&args.campaign_id).join("state.json");
let state = match CampaignState::load(&state_path) {
let state = match CampaignState::load_campaign(&args.campaign_id) {
Ok(s) => s,
Err(e) => {
log::error!("{e}");
log::error!(
"campaign '{}' not found; list campaigns with: ls {}",
args.campaign_id,
runs_dir.display()
);
return false;
}
};
Expand Down
32 changes: 10 additions & 22 deletions smitebot/src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,6 @@ impl StartCommand {
}
};

let Some(runs_dir) = CampaignState::runs_dir() else {
log::error!("unable to determine home directory");
return false;
};

let state_path = runs_dir.join(&campaign_id).join("state.json");

let Some(git_hash) = smite_git_hash(&config.smite_dir) else {
log::error!("could not determine smite git hash");
return false;
Expand All @@ -143,21 +136,21 @@ impl StartCommand {
tmux_session,
);

if let Err(e) = state.save(&state_path) {
if let Err(e) = state.save_campaign() {
log::error!("{e}");
return false;
}

if !launch_runners(&config, &seed_dir, &mut state, &state_path) {
if !launch_runners(&config, &seed_dir, &mut state) {
return false;
}
if let Err(e) = state.save(&state_path) {
if let Err(e) = state.save_campaign() {
log::error!("{e}");
return false;
}

log::info!("campaign {} is running", state.id);
log::info!("state saved to {}", state_path.display());
log::info!("state saved to ~/.smitebot/runs/{}/state.json", state.id);

log::info!("attaching to tmux session '{}'", state.tmux_session);
if let Err(e) = tmux::attach(&state.tmux_session) {
Expand All @@ -170,12 +163,7 @@ impl StartCommand {

/// Spawns all runners inside a tmux session, verifies they produce
/// `fuzzer_stats`, and updates campaign state with PIDs.
fn launch_runners(
config: &CampaignConfig,
seed_dir: &Path,
state: &mut CampaignState,
state_path: &Path,
) -> bool {
fn launch_runners(config: &CampaignConfig, seed_dir: &Path, state: &mut CampaignState) -> bool {
let session = &state.tmux_session;
log::info!(
"starting {} runners in tmux session '{session}'",
Expand All @@ -198,7 +186,7 @@ fn launch_runners(
if let Err(e) = result {
log::error!("failed to create tmux window for runner {id}: {e}");
state.runners = runners;
fail_campaign(state, state_path);
fail_campaign(state);
return false;
}

Expand All @@ -207,7 +195,7 @@ fn launch_runners(

state.runners = runners;

if let Err(e) = state.save(state_path) {
if let Err(e) = state.save_campaign() {
log::warn!("failed to save state: {e}");
}

Expand All @@ -221,7 +209,7 @@ fn launch_runners(
// verify_startup has already logged the specific reason per runner
// (window died, or ceiling reached).
log::error!("one or more runners failed to start");
fail_campaign(state, state_path);
fail_campaign(state);
return false;
}

Expand All @@ -231,7 +219,7 @@ fn launch_runners(

/// Marks the campaign as failed, logs instructions to inspect the tmux session,
/// and persists the updated state.
fn fail_campaign(state: &mut CampaignState, state_path: &Path) {
fn fail_campaign(state: &mut CampaignState) {
if !state.runners.is_empty() {
log::info!(
"inspect tmux session '{}' for error output, \
Expand All @@ -241,7 +229,7 @@ fn fail_campaign(state: &mut CampaignState, state_path: &Path) {
);
}
state.status = Status::Failed;
if let Err(e) = state.save(state_path) {
if let Err(e) = state.save_campaign() {
log::warn!("failed to save state: {e}");
}
}
Expand Down
15 changes: 2 additions & 13 deletions smitebot/src/commands/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,10 @@ impl StatusCommand {
/// Reports the status of a campaign, either as a one-shot summary or by
/// attaching to its live tmux dashboard.
pub fn execute(args: &StatusArgs) -> bool {
let Some(runs_dir) = CampaignState::runs_dir() else {
log::error!("unable to determine home directory");
return false;
};
let state_path = runs_dir.join(&args.campaign_id).join("state.json");

let state = match CampaignState::load(&state_path) {
Ok(state) => state,
let state = match CampaignState::load_campaign(&args.campaign_id) {
Ok(s) => s,
Err(e) => {
log::error!("{e}");
log::error!(
"campaign '{}' not found; list campaigns with: ls {}",
args.campaign_id,
runs_dir.display()
);
return false;
}
};
Expand Down
17 changes: 3 additions & 14 deletions smitebot/src/commands/stop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,10 @@ impl StopCommand {
/// Stops a campaign: reaps its runner process groups, tears down the tmux
/// session, and records the stop in state.json.
pub fn execute(args: &StopArgs) -> bool {
let Some(runs_dir) = CampaignState::runs_dir() else {
log::error!("unable to determine home directory");
return false;
};
let state_path = runs_dir.join(&args.campaign_id).join("state.json");

let mut state = match CampaignState::load(&state_path) {
Ok(state) => state,
let mut state = match CampaignState::load_campaign(&args.campaign_id) {
Ok(s) => s,
Err(e) => {
log::error!("{e}");
log::error!(
"campaign '{}' not found; list campaigns with: ls {}",
args.campaign_id,
runs_dir.display()
);
return false;
}
};
Expand All @@ -74,7 +63,7 @@ impl StopCommand {

state.status = Status::Stopped;
state.stop_time = Some(utils::epoch_secs());
if let Err(e) = state.save(&state_path) {
if let Err(e) = state.save_campaign() {
log::error!(
"runners were reaped but recording the stop failed: {e}; \
campaign {} will still show as running in state.json",
Expand Down
23 changes: 21 additions & 2 deletions smitebot/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ pub enum StateError {
path: PathBuf,
source: serde_json::Error,
},
#[error("unable to determine home directory")]
HomeDir,
}

impl CampaignState {
Expand Down Expand Up @@ -141,7 +143,7 @@ impl CampaignState {

/// Saves the campaign state as JSON, using an atomic write to prevent
/// corruption if the process is interrupted.
pub fn save(&self, path: &Path) -> Result<(), StateError> {
fn save(&self, path: &Path) -> Result<(), StateError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|source| StateError::CreateDir {
path: parent.to_path_buf(),
Expand All @@ -163,8 +165,14 @@ impl CampaignState {
Ok(())
}

/// Saves the campaign state to `~/.smitebot/runs/<id>/state.json`.
pub fn save_campaign(&self) -> Result<(), StateError> {
let runs_dir = Self::runs_dir().ok_or(StateError::HomeDir)?;
self.save(&runs_dir.join(&self.id).join("state.json"))
}

/// Loads campaign state from a JSON file written by `save`.
pub fn load(path: &Path) -> Result<Self, StateError> {
fn load(path: &Path) -> Result<Self, StateError> {
let contents = fs::read_to_string(path).map_err(|source| StateError::Read {
path: path.to_path_buf(),
source,
Expand All @@ -174,6 +182,12 @@ impl CampaignState {
source,
})
}

/// Loads state for campaign `id` from `~/.smitebot/runs/<id>/state.json`.
pub fn load_campaign(id: &str) -> Result<Self, StateError> {
let runs_dir = Self::runs_dir().ok_or(StateError::HomeDir)?;
Self::load(&runs_dir.join(id).join("state.json"))
}
}

#[cfg(test)]
Expand Down Expand Up @@ -325,6 +339,11 @@ sharedir = "/tmp/nyx"
assert_eq!(loaded.stop_time, Some(1_749_469_200));
}

#[test]
fn load_campaign_returns_err_for_missing_campaign() {
assert!(CampaignState::load_campaign("nonexistent-campaign-xkcd-abc123").is_err());
}

#[test]
fn load_reports_missing_file() {
let err = CampaignState::load(Path::new("/no/such/state.json")).unwrap_err();
Expand Down
Loading