diff --git a/Cargo.lock b/Cargo.lock index a29b1a9..729b1f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,15 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -422,6 +431,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "2.0.1" @@ -964,6 +979,8 @@ dependencies = [ name = "ovlach_pdf" version = "0.1.0" dependencies = [ + "async-mutex", + "async-trait", "fern", "headless_chrome", "log", @@ -971,6 +988,7 @@ dependencies = [ "rocket", "serde", "serde_yaml", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 990b417..fb1e188 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,6 @@ rocket = { version = "0.5.0", features = ["json"] } headless_chrome = "1.0.8" ovlach_data = { git = "ssh://git@gitlab.nanobyte.cz/ondrej/ov-site-api-data.git", branch = "add_missing_fields"} +async-trait = "0.1.74" +async-mutex = "1.4.0" +tokio = { version = "1.34.0", features = ["macros"] } \ No newline at end of file diff --git a/src/chromium/mod.rs b/src/chromium/mod.rs new file mode 100644 index 0000000..78211d4 --- /dev/null +++ b/src/chromium/mod.rs @@ -0,0 +1 @@ +pub mod rocket; \ No newline at end of file diff --git a/src/chromium/rocket.rs b/src/chromium/rocket.rs new file mode 100644 index 0000000..91a7c00 --- /dev/null +++ b/src/chromium/rocket.rs @@ -0,0 +1,127 @@ +use std::{sync::{Arc}, time::Duration}; +use async_trait::async_trait; +use async_mutex::Mutex; +use headless_chrome::{Browser, LaunchOptions}; +use log::{warn, error}; +use rocket::{fairing::{Fairing, self}, Rocket, Build, catcher::Result, Request, Response, request::{FromRequest, Outcome}}; + +#[derive(Default)] +pub struct Chromium { +} + +impl Chromium { + pub fn ignite() -> Self { + Self::default() + } + + pub fn default() -> Self { + Self { + } + } +} + +#[async_trait] +impl Fairing for Chromium { + fn info(&self) -> fairing::Info { + fairing::Info { + name: "Chromium", + kind: fairing::Kind::Ignite, + } + } + + async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { + let new_rocket = rocket.manage(ChromiumCoordinator::new().await); + let coordinator = new_rocket.state::().unwrap(); + error!("{:p}", &coordinator); + Ok(new_rocket) + } +} +pub struct ChromiumCoordinator { + instances: Arc>>, +} + +impl ChromiumCoordinator { + const NUMBER_OF_INSTANCES: usize = 1; + + pub async fn new() -> Self { + error!("ChromiumCoordinator::new()"); + let instances: Arc>> = Arc::new(Mutex::new(Vec::with_capacity(Self::NUMBER_OF_INSTANCES))); + while instances.lock().await.len() < Self::NUMBER_OF_INSTANCES { + instances.lock().await.push(BrowserHolder { browser: Browser::new(LaunchOptions { + idle_browser_timeout: Duration::from_secs(1), // Wait inifinity for commands + ..LaunchOptions::default() + }).unwrap() }); + } + + Self { instances } + } + + fn spawn_browser(&self) { + let instances = self.instances.clone(); + tokio::spawn(async move { + error!("Spawn new instance of browser"); + // Create new instance + let browser = Browser::new(LaunchOptions::default()).unwrap(); + instances.lock().await.push( BrowserHolder {browser }); + }); + } + + fn try_get_instance(&self) -> Option { + let mut instances = self.instances.lock().await; + if instances.len() == 0 { + return None; + } + Some(instances.remove(0)) + } + + pub async fn try_get_browser(&self) -> std::result::Result { + let instances = self.instances.clone(); + + let mut loop_count = 0; + + while instances.lock().await.len() == 0 { + warn!("Waiting for Chromium instances to start..."); + std::thread::sleep(std::time::Duration::from_millis(100)); + if loop_count > 100 * 10 * 60 { + panic!("Can't start Chromium instances"); + } + loop_count += 1; + } + + let browser = self.instances.lock().await.remove(0); + self.spawn_browser(); + // test connection state + match browser.browser.get_version() { + Ok(_) => Ok(browser), + Err(_) => { + Err(()) + } + } + } + + pub async fn get_browser(&self) -> std::result::Result { + loop { + match self.try_get_browser().await { + Ok(browser) => return Ok(browser), + Err(_) => {} // all instances may be dead ... we must wait for new instance + } + } + } +} + +pub struct BrowserHolder { + pub browser: Browser, +} + +#[async_trait] +impl<'r> FromRequest<'r> for BrowserHolder { + type Error = (); + + async fn from_request(request: &'r Request<'_>) -> Outcome { + let coordinator = request.rocket().state::().unwrap(); + error!("{:p}", &coordinator); + let browser = coordinator.get_browser().await.unwrap(); + Outcome::Success(browser) + } +} + diff --git a/src/lib.rs b/src/lib.rs index ccfe2a4..392e7df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,13 @@ +use chromium::rocket::Chromium; use rocket::{Rocket, Build, routes}; pub mod routes; +mod chromium; pub fn rocket_builder() -> Rocket { - rocket::build().mount("/", routes![ - routes::pdf::render_pdf_cv, + rocket::build() + .attach(Chromium::ignite()) + .mount("/", routes![ + routes::pdf::render_pdf_cv, ]) } \ No newline at end of file diff --git a/src/routes/pdf.rs b/src/routes/pdf.rs index e406117..8efe78b 100644 --- a/src/routes/pdf.rs +++ b/src/routes/pdf.rs @@ -7,8 +7,9 @@ use headless_chrome::{types::PrintToPdfOptions, LaunchOptions}; use log::error; use rocket::{get, Response, futures::Stream, tokio::net::UnixStream, fs::NamedFile}; -fn generate_pdf() { - let browser = Browser::new(LaunchOptions::default()).unwrap(); +use crate::chromium::rocket::BrowserHolder; + +fn generate_pdf(browser: Browser) { let tab = browser.new_tab().unwrap(); let tab = tab.navigate_to("file:///home/6a6996c0-1609-48b6-8ca6-affbef1b4d1d/Devel/Nanobyte/ovlach/ovlach_pdf/template.html").unwrap().wait_until_navigated().unwrap(); let options = PrintToPdfOptions{ @@ -34,8 +35,8 @@ fn generate_pdf() { } #[get("/cv//pdf")] -pub async fn render_pdf_cv(username: &str) -> NamedFile { - generate_pdf(); +pub async fn render_pdf_cv(username: &str, browser: BrowserHolder) -> NamedFile { + generate_pdf(browser.browser); "foo!".to_string(); NamedFile::open("/tmp/foo.pdf").await.expect("failed to open foo.pdf") } \ No newline at end of file