Skip to content

Commit 5d98c88

Browse files
committed
add job polling and analysis naming
1 parent bdece37 commit 5d98c88

4 files changed

Lines changed: 181 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.1.0"
44
authors = ["Dylan Halperin <dylan@dylemma.io>"]
55

66
[dependencies]
7+
chrono = "0.4"
78
clap = "2.26.2"
89
hyper = "0.11"
910
maplit = "0.1.4"

src/client.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ use serde::ser::Serialize;
66
use serde_json;
77
use std;
88
use std::collections::HashMap;
9+
use std::fmt::Debug;
910
use std::io::Read;
1011
use std::path::Path;
12+
use std::thread;
13+
use std::time::Duration;
1114

1215

1316
/// Project filter criteria used with `ApiClient::query_projects` to define project filter criteria.
@@ -61,7 +64,47 @@ pub struct ApiAnalysisJobResponse {
6164
pub job_id: String
6265
}
6366

67+
/// Enumeration representing the 5 possible statuses a Code Dx "job" may be in.
68+
#[serde(rename_all = "lowercase")]
69+
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
70+
pub enum JobStatus {
71+
Queued,
72+
Running,
73+
Cancelled,
74+
Completed,
75+
Failed
76+
}
77+
impl JobStatus {
78+
pub fn is_ready(&self) -> bool {
79+
match *self {
80+
JobStatus::Completed => true,
81+
JobStatus::Failed => true,
82+
_ => false
83+
}
84+
}
85+
pub fn is_success(&self) -> bool {
86+
match *self {
87+
JobStatus::Completed => true,
88+
_ => false
89+
}
90+
}
91+
}
92+
93+
#[derive(Debug, Deserialize, Serialize)]
94+
pub struct JobStatusResponse {
95+
/// ID of the requested job.
96+
///
97+
/// This should be the same as the `job_id` sent when you requested the status in the first place.
98+
#[serde(rename = "jobId")]
99+
pub job_id: String,
100+
101+
/// The actual job status.
102+
pub status: JobStatus,
64103

104+
// there are some optional fields like "progress", "blockedBy", and "reason"
105+
// which are present depending on the status, but they aren't necessary for
106+
// our use case, so I'm not going to model them.
107+
}
65108

66109
/// Things that can go wrong when making requests with the API.
67110
#[derive(Debug)]
@@ -118,6 +161,25 @@ struct ErrorMessageResponse {
118161
error: String
119162
}
120163

164+
/// Defines a polling strategy based on the iteration number and current state of the poll.
165+
///
166+
/// The `next_wait` function decides how long the polling process should wait before re-checking the state.
167+
/// If it returns `Some(duration)`, the polling process will wait that duration before re-checking.
168+
/// If it returns `None`, the polling process will immediately end, typically returning the latest state.
169+
///
170+
/// The `iteration_number` will start at `1` and increment every time `next_wait` is called for the current poll.
171+
pub trait PollingStrategy<T> {
172+
fn next_wait(&self, iteration_number: usize, state: &T) -> Option<Duration>;
173+
}
174+
175+
/// Simple polling strategy that always waits a fixed amount of time between iterations.
176+
impl <T: Debug> PollingStrategy<T> for Duration {
177+
fn next_wait(&self, iteration_number: usize, state: &T) -> Option<Duration> {
178+
println!("in poll (iteration {}, state: {:?})", iteration_number, state);
179+
Some(*self)
180+
}
181+
}
182+
121183
pub type ApiResult<T> = Result<T, ApiError>;
122184

123185

@@ -176,6 +238,46 @@ impl ApiClient {
176238
ApiClient { config, client }
177239
}
178240

241+
pub fn get_job_status(&self, job_id: &str) -> ApiResult<JobStatus> {
242+
self.api_get(&["api", "jobs", job_id])
243+
.expect_success()
244+
.expect_json::<JobStatusResponse>()
245+
.map(|jsr| jsr.status)
246+
}
247+
248+
/// Repeatedly call `get_job_status(job_id)` until it returns an error or a "ready" status.
249+
///
250+
/// Uses the provided `polling_stategy` to determine how long to wait between each status
251+
/// check, and whether to abort early.
252+
///
253+
/// If the `polling_strategy` decides to abort early, the result of the poll will be the
254+
/// most recent `JobStatus` to be passed.
255+
///
256+
/// If at any point the job status check fails (i.e. `get_job_status` returns an `Err(_)`),
257+
/// the poll will immediately stop, returning that error.
258+
pub fn poll_job_completion<P: PollingStrategy<JobStatus>>(&self, job_id: &str, polling_strategy: P) -> ApiResult<JobStatus> {
259+
let mut iteration_number: usize = 0;
260+
loop {
261+
let status_result = self.get_job_status(job_id);
262+
iteration_number += 1;
263+
match status_result {
264+
Ok(status) => {
265+
if status.is_ready() {
266+
break status_result;
267+
} else {
268+
// call the "step" function to see if the poll should continue,
269+
// and if so, how long it should wait before checking again
270+
match polling_strategy.next_wait(iteration_number, &status) {
271+
Some(wait_dur) => thread::sleep(wait_dur),
272+
None => break status_result,
273+
}
274+
}
275+
},
276+
Err(_) => break status_result,
277+
}
278+
}
279+
}
280+
179281
pub fn get_projects(&self) -> ApiResult<Vec<ApiProject>> {
180282
self.api_get(&["x", "projects"])
181283
.expect_success()
@@ -204,6 +306,13 @@ impl ApiClient {
204306
})
205307
}
206308

309+
pub fn set_analysis_name(&self, project_id: u32, analysis_id: u32, name: &str) -> ApiResult<()> {
310+
self.api_put(&["x", "projects", &project_id.to_string(), "analyses", &analysis_id.to_string()], json!({ "name": name }))
311+
.expect_success()
312+
.get()
313+
.map(|_| ())
314+
}
315+
207316
pub fn api_get(&self, path_segments: &[&str]) -> ApiResponse {
208317
self.api_request(Method::Get, path_segments, ReqBody::None)
209318
}

src/main.rs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
extern crate chrono;
12
extern crate clap;
23
extern crate reqwest;
34
extern crate serde;
@@ -8,7 +9,9 @@ extern crate url;
89
#[macro_use] extern crate serde_json;
910
#[macro_use] extern crate serde_derive;
1011

12+
use chrono::Utc;
1113
use std::path::Path;
14+
use std::time::Duration;
1215

1316
mod config;
1417
use config::*;
@@ -45,14 +48,31 @@ fn main() {
4548
Err(e) => println!("Error querying projects: {:?}", e),
4649
}
4750

48-
// Start an analysis (currently hardcoded project id and files) and println the server's response
51+
// Start an analysis (currently hardcoded project id and files), set its name, and poll until it finishes
4952
println!();
50-
let analysis_job_result = client.start_analysis(107, vec![
51-
Path::new("D:/CodeDx/data-sets/webgoat eval/bin-webgoat-r437.zip"),
52-
Path::new("D:/CodeDx/data-sets/webgoat eval/src-webgoat-r437.zip")
53-
]);
54-
match analysis_job_result {
55-
Ok(response) => println!("Started analysis: {:?}", response),
53+
let project_id = 4;
54+
let analysis_result = client
55+
.start_analysis(project_id, vec![
56+
Path::new("D:/CodeDx/data-sets/webgoat eval/bin-webgoat-r437.zip"),
57+
Path::new("D:/CodeDx/data-sets/webgoat eval/src-webgoat-r437.zip")
58+
])
59+
.and_then(|analysis_job_response| {
60+
let analysis_id = analysis_job_response.analysis_id;
61+
println!("Started analysis: {}", analysis_id);
62+
let name = format!("My CLI Analysis @ {}", format_current_datetime());
63+
64+
client.set_analysis_name(project_id, analysis_id, &name)
65+
.map(|_| {
66+
println!("Set analysis name to {}", name);
67+
analysis_job_response
68+
})
69+
})
70+
.and_then(|analysis_job_response| {
71+
let job_id = analysis_job_response.job_id;
72+
client.poll_job_completion(&job_id, Duration::from_secs(2))
73+
});
74+
match analysis_result {
75+
Ok(status) => println!("Analysis finished with status {:?}", status),
5676
Err(e) => println!("Couldn't start analysis: {:?}", e),
5777
}
5878

@@ -66,4 +86,6 @@ fn main() {
6686
};
6787
}
6888

69-
89+
fn format_current_datetime() -> String {
90+
Utc::now().format("%Y-%m-%d %H:%M:%S").to_string()
91+
}

0 commit comments

Comments
 (0)