use crate::result::{PebblesError, PebblesErrorDetails};
use reqwest::blocking::{Client, Response};
use serde_json::Value;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::process::{Child, Command, Stdio};
pub enum RequestType {
Post,
Get,
Delete,
}
pub struct Service {
port: u16,
session_id: String,
client: Client,
process: Child,
}
impl Service {
fn find_free_port() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to a socket");
let port = listener.local_addr().unwrap().port();
drop(listener);
port
}
fn launch_gecko(command: &str, port: &u16) -> Child {
let process = Command::new(command)
.arg("--port")
.arg(port.to_string())
.stdout(Stdio::null())
.spawn()
.expect("Failed to launch process");
process
}
fn accepting_connections(port: &u16) -> bool {
let address: SocketAddr = format!("127.0.0.1:{}", port.to_string()).parse().unwrap();
match TcpStream::connect_timeout(&address, std::time::Duration::from_secs(1)) {
Ok(_) => true,
Err(_) => false,
}
}
fn launch_firefox(
client: &Client,
port: &u16,
capabilities: &Value,
) -> Result<String, PebblesError> {
let response = client
.post(format!("http://localhost:{}/session", port.to_string()))
.header("Content-Type", "application/json")
.body(match serde_json::to_string(&capabilities) {
Ok(json_string) => json_string,
Err(_) => {
return Err(PebblesError::NewSessionError(PebblesErrorDetails::new(
"Capabilities not in Valid json Format",
)))
}
})
.send()?;
if !response.status().is_success() {
return Err(PebblesError::NewSessionError(PebblesErrorDetails::new(
format!(
"Session Start Request Failed: /
Status Code: {}",
response.status().as_u16()
),
)));
}
let response_json: Value = response.json::<Value>()?;
match response_json["value"]["sessionId"].as_str() {
Some(session_id) => Ok(session_id.to_string()),
None => Err(PebblesError::NewSessionError(PebblesErrorDetails::new(
"Faild not load session ID from response json",
))),
}
}
pub fn new(capabilities: &Value, geckodriver_cmd: &str) -> Result<Service, PebblesError> {
let port = Self::find_free_port();
let mut process = Self::launch_gecko(geckodriver_cmd, &port);
while !Self::accepting_connections(&port) {
if let Some(_) = process.try_wait().expect("Failed to get process status") {
return Err(PebblesError::NewSessionError(PebblesErrorDetails::new(
"Process has Exited Early",
)));
}
}
let client = Client::new();
let session_id = Self::launch_firefox(&client, &port, &capabilities)?;
Ok(Service {
port: port,
session_id: session_id,
client: client,
process: process,
})
}
fn request(
&self,
method: RequestType,
path: &str,
body: Option<&Value>,
) -> Result<Response, PebblesError> {
let url = format!(
"http://localhost:{}/session/{}{}",
self.port.to_string(),
&self.session_id,
path
);
let mut request = match method {
RequestType::Post => self.client.post(url),
RequestType::Get => self.client.get(url),
RequestType::Delete => self.client.delete(url),
};
request = request.header("Content-Type", "application/json");
if let Some(body) = body {
request = request.body(match serde_json::to_string(body) {
Ok(json_string) => json_string,
Err(_) => {
return Err(PebblesError::RequestError(PebblesErrorDetails::new(
"Unable to format body as String",
)))
}
});
}
let response = request.send()?;
if !response.status().is_success() {
return Err(PebblesError::RequestError(PebblesErrorDetails::new(
format!(
"Request Failed: Status /
Code: {}/
Body: {}",
response.status().as_u16(),
match response.text() {Ok(text) => text, Err(_) => "None".to_string()},
),
)));
}
Ok(response)
}
pub fn post(&self, path: &str, body: &Value) -> Result<Response, PebblesError> {
return self.request(RequestType::Post, path, Some(body));
}
pub fn get(&self, path: &str) -> Result<Response, PebblesError> {
return self.request(RequestType::Get, path, None);
}
pub fn delete(&self, path: &str) -> Result<Response, PebblesError> {
return self.request(RequestType::Delete, path, None);
}
}
impl Drop for Service {
fn drop(&mut self) {
let _ = self.delete("");
match self.process.kill() {
Ok(_) => println!("Process killed successfully"),
Err(e) => println!("Failed to kill process: {}", e),
}
}
}