diff --git a/Usage.md b/Usage.md index 0316dc0..aa4d748 100644 --- a/Usage.md +++ b/Usage.md @@ -10,6 +10,7 @@ This document contains the help content for the `oseda` command-line program. * [`oseda check`↴](#oseda-check) * [`oseda deploy`↴](#oseda-deploy) * [`oseda fork`↴](#oseda-fork) +* [`oseda export`↴](#oseda-export) ## `oseda` @@ -24,6 +25,7 @@ oseda project scafolding CLI * `check` — Check the Oseda project in the working directory for common errors * `deploy` — Deploy your Oseda project to github to add to oseda.net * `fork` — Fork the library repository to submit your course +* `export` — Export the Oseda project to a PDF file This will install the npm package `decktape` This relies on a chromium backend, as a result, it may take a while to run @@ -84,6 +86,23 @@ Fork the library repository to submit your course +## `oseda export` + +Export the Oseda project to a PDF file This will install the npm package `decktape` This relies on a chromium backend, as a result, it may take a while to run + +**Usage:** `oseda export [OPTIONS]` + +###### **Options:** + +* `--output ` — String name of the output PDF file + + Default value: `slides.pdf` +* `--port ` — Port the project runs on + + Default value: `3000` + + +
diff --git a/scripts/test-init.sh b/scripts/test-init.sh index ab26c40..8b5789e 100755 --- a/scripts/test-init.sh +++ b/scripts/test-init.sh @@ -13,3 +13,5 @@ cd test pwd ./oseda init --title ExampleProject --tags economics ComPuterScience --color red --template MaRKDoWN + +mv oseda ExampleProject \ No newline at end of file diff --git a/src/bin/oseda.rs b/src/bin/oseda.rs index d8acece..330bbd1 100644 --- a/src/bin/oseda.rs +++ b/src/bin/oseda.rs @@ -5,6 +5,7 @@ use oseda_cli::{ cmd::{ check, deploy::{self}, + export::{self}, fork::{self}, init, run, }, @@ -31,6 +32,8 @@ fn main() { println!("See deployment instructions..."); }), Commands::Fork => fork::fork(), + Commands::Export(options) => export::export(options.clone()) + .map(|_| println!("Successfully export project to {0}", options.port)), }; // little annoying, but makes the exit code match what users would expect diff --git a/src/cmd/export.rs b/src/cmd/export.rs new file mode 100644 index 0000000..9687bbe --- /dev/null +++ b/src/cmd/export.rs @@ -0,0 +1,75 @@ +use std::{ + error::Error, + process::Command, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +use clap::Args; + +use crate::{cmd::run, net::kill_port}; + +/// Options struct for the export subcommand +#[derive(Args, Debug, Clone)] +pub struct ExportOptions { + /// String name of the output PDF file + #[arg(long, default_value = "slides.pdf")] + pub output: String, + /// Port the project runs on + #[arg(long, default_value_t = 3000)] + pub port: u16, +} + +/// Export the current Oseda project to a PDF file via `decktape` +pub fn export(opts: ExportOptions) -> Result<(), Box> { + if kill_port(opts.port).is_err() { + eprintln!("Warning, could not kill value on desired port") + } + + let output = Command::new("npm") + .args(["install", "decktape@3.15.0"]) + .current_dir(".") + .output()?; + + if !output.status.success() { + eprintln!( + "Decktape installation failure: {}", + String::from_utf8_lossy(&output.stderr) + ); + return Err("npm init failed".into()); + } + + // decktape automatic http://localhost:3000/ Desktop/IntroToRust/slides.pdf + + let shutdown_flag = Arc::new(AtomicBool::new(false)); + let run_flag = shutdown_flag.clone(); + + let run_handle = std::thread::spawn(move || run::run_with_shutdown(run_flag)); + + // wait a moment for the localhost server to spin up + std::thread::sleep(std::time::Duration::from_millis(10000)); + + let addr = format!("http://localhost:{}", opts.port); + + // run decktape, assuming the server has spun up by now + let export_output = Command::new("decktape") + .args(["automatic", &addr, &opts.output]) + .output()?; + + // send shutdown flag, should signal to run_with_shutdown to kill the process + shutdown_flag.store(true, Ordering::SeqCst); + // wait to run to terminate (hopefully gracefully) and join the process to cur. thread + let _ = run_handle.join(); + + if !export_output.status.success() { + eprintln!( + "Decktape PDF export failure: {}", + String::from_utf8_lossy(&export_output.stderr) + ); + return Err("npm init failed".into()); + } + + Ok(()) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 531c54c..d63b2d2 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,5 +1,6 @@ pub mod check; pub mod deploy; +pub mod export; pub mod fork; pub mod init; pub mod run; diff --git a/src/cmd/run.rs b/src/cmd/run.rs index d5c6de1..a059aff 100644 --- a/src/cmd/run.rs +++ b/src/cmd/run.rs @@ -1,4 +1,11 @@ -use std::{process::Command, sync::mpsc}; +use std::{ + process::Command, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; /// More in depth errors that could cause a project not to run #[derive(Debug)] @@ -28,6 +35,11 @@ impl std::fmt::Display for OsedaRunError { /// * `Ok(())` if both the build and serve steps succeed /// * `Err(OsedaRunError)` if any step fails (missing vite isn't installed, or `serve` fails to start) pub fn run() -> Result<(), OsedaRunError> { + // todo refactor the other check command to use this + run_with_shutdown(Arc::new(AtomicBool::new(false))) +} + +pub fn run_with_shutdown(shutdown_flag: Arc) -> Result<(), OsedaRunError> { // command run failure and command status are considered different, handled accordingly match Command::new("npx").arg("vite").arg("build").status() { Ok(status) => { @@ -59,15 +71,21 @@ pub fn run() -> Result<(), OsedaRunError> { // spawn will leave child running the background. Need to listen for ctrl+c, snatch it. Then kill subprocess // https://github.com/Detegr/rust-ctrlc - let (tx, rx) = mpsc::channel(); + // let (tx, rx) = mpsc::channel(); + let ctrlc_flag = shutdown_flag.clone(); ctrlc::set_handler(move || { println!("\nSIGINT received. Attempting graceful shutdown..."); - let _ = tx.send(()); + ctrlc_flag.store(true, Ordering::SeqCst); }) - .expect("Error setting Ctrl+C handler"); + .map_err(|e| { + println!("Error setting ctrl+c handler: {e}"); + OsedaRunError::ServeError("failed to set handler".into()) + })?; - // block until ctrl+c - rx.recv().unwrap(); + // block until ctrl+c or sigkill or flag set otherwise (e.g. via export) + while !shutdown_flag.load(Ordering::SeqCst) { + std::thread::sleep(Duration::from_millis(100)); + } // attempt to kill the child process if let Err(e) = child.kill() { diff --git a/src/lib.rs b/src/lib.rs index e337881..4cea8fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,4 +33,8 @@ pub enum Commands { Deploy(cmd::deploy::DeployOptions), /// Fork the library repository to submit your course Fork, + /// Export the Oseda project to a PDF file + /// This will install the npm package `decktape` + /// This relies on a chromium backend, as a result, it may take a while to run + Export(cmd::export::ExportOptions), }