use clap::{Parser, Subcommand};
use rfheadless::Engine;
use std::io::{self, BufRead, Write};
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Run {
url: String,
#[clap(long)]
screenshot: Option<String>,
#[clap(long, action = clap::ArgAction::SetTrue)]
no_js: bool,
#[clap(long, default_value_t = 30000)]
timeout_ms: u64,
#[clap(long)]
stylesheet_concurrency: Option<usize>,
#[clap(long, action = clap::ArgAction::SetTrue)]
disable_persistent_runtime: bool,
},
Eval { script: String },
Screenshot { path: String },
Abort,
Cookies {
#[clap(subcommand)]
action: CookieAction,
},
Config {
#[clap(subcommand)]
action: ConfigAction,
},
}
#[derive(Subcommand)]
enum CookieAction {
List,
Set {
name: String,
value: String,
#[clap(long)]
url: Option<String>,
#[clap(long)]
domain: Option<String>,
#[clap(long)]
path: Option<String>,
},
Delete {
name: String,
#[clap(long)]
url: Option<String>,
#[clap(long)]
domain: Option<String>,
#[clap(long)]
path: Option<String>,
},
Clear,
}
#[derive(Subcommand)]
enum ConfigAction {
Show,
SetConcurrency { value: usize },
SetPersistent { enabled: bool },
}
fn worker_main() -> io::Result<()> {
use serde::Deserialize;
use serde::Serialize;
#[derive(Deserialize)]
struct Job {
id: u64,
code: String,
loop_limit: u64,
recursion_limit: usize,
}
#[derive(Serialize)]
struct Res {
id: u64,
value: String,
is_error: bool,
}
let stdin = io::stdin();
let stdout = io::stdout();
let mut out = stdout.lock();
let mut ctx: boa_engine::Context = boa_engine::Context::default();
for line in stdin.lock().lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
if let Ok(job) = serde_json::from_str::<Job>(&line) {
if job.loop_limit > 0 {
ctx.runtime_limits_mut()
.set_loop_iteration_limit(job.loop_limit);
}
if job.recursion_limit < usize::MAX {
ctx.runtime_limits_mut()
.set_recursion_limit(job.recursion_limit);
}
let res = match ctx.eval(boa_engine::Source::from_bytes(job.code.as_bytes())) {
Ok(v) => Res {
id: job.id,
value: format!("{}", v.display()),
is_error: false,
},
Err(e) => Res {
id: job.id,
value: format!("Script thrown: {}", e),
is_error: true,
},
};
let js = serde_json::to_string(&res).unwrap_or_else(|_| {
format!(
"{{\"id\":{},\"value\":\"serialization failed\",\"is_error\":true}}",
job.id
)
});
writeln!(out, "{}", js)?;
out.flush()?;
} else {
}
}
Ok(())
}
fn run_cli_cmd(run: Commands) -> Result<(), Box<dyn std::error::Error>> {
match run {
Commands::Run {
url,
screenshot,
no_js,
timeout_ms,
stylesheet_concurrency,
disable_persistent_runtime,
} => {
let cfg = rfheadless::EngineConfig {
enable_javascript: !no_js,
timeout_ms,
stylesheet_fetch_concurrency: stylesheet_concurrency.unwrap_or_default(),
enable_persistent_runtime: !disable_persistent_runtime,
..Default::default()
};
let mut engine = rfheadless::new_engine(cfg)?;
engine.load_url(&url)?;
let snap = engine.render_text_snapshot()?;
println!(
"Title: {}\nURL: {}\nText preview:\n{}",
snap.title,
snap.url,
&snap.text.chars().take(400).collect::<String>()
);
if let Some(path) = screenshot {
match engine.render_png() {
Ok(p) => {
let _ = std::fs::write(path, p);
println!("Screenshot saved");
}
Err(e) => eprintln!("Screenshot failed: {}", e),
}
}
engine.close()?;
}
Commands::Eval { script } => {
let cfg = rfheadless::EngineConfig::default();
let mut engine = rfheadless::new_engine(cfg)?;
match engine.evaluate_script(&script) {
Ok(res) => println!("Result: {} (is_error={})", res.value, res.is_error),
Err(e) => eprintln!("Eval failed: {}", e),
}
let _ = engine.close();
}
Commands::Screenshot { path } => {
let cfg = rfheadless::EngineConfig::default();
let engine = rfheadless::new_engine(cfg)?;
match engine.render_png() {
Ok(p) => {
let _ = std::fs::write(path, p);
println!("Screenshot saved");
}
Err(e) => eprintln!("Screenshot failed: {}", e),
}
let _ = engine.close();
}
Commands::Abort => {
#[cfg(feature = "rfengine")]
{
let cfg = rfheadless::EngineConfig::default();
let mut engine = rfheadless::rfengine::RFEngine::new(cfg)?;
if let Err(e) = engine.abort_running_script() {
eprintln!("Abort failed: {}", e);
} else {
println!("Abort requested");
}
let _ = engine.close();
}
#[cfg(not(feature = "rfengine"))]
{
eprintln!("Abort command requires the 'rfengine' feature (compile with --features rfengine)");
}
}
Commands::Cookies { action } => {
let cfg = rfheadless::EngineConfig::default();
let mut engine = rfheadless::new_engine(cfg)?;
match action {
CookieAction::List => match engine.get_cookies() {
Ok(c) => {
for ck in c {
println!("{}={} (domain={:?})", ck.name, ck.value, ck.domain);
}
}
Err(e) => eprintln!("Failed to list cookies: {}", e),
},
CookieAction::Set {
name,
value,
url,
domain,
path,
} => {
let param = rfheadless::CookieParam {
name,
value,
url,
domain,
path,
secure: None,
http_only: None,
same_site: None,
expires: None,
};
if let Err(e) = engine.set_cookies(vec![param]) {
eprintln!("Failed to set cookie: {}", e);
} else {
println!("Cookie set");
}
}
CookieAction::Delete {
name,
url,
domain,
path,
} => {
if let Err(e) = engine.delete_cookie(
&name,
url.as_deref(),
domain.as_deref(),
path.as_deref(),
) {
eprintln!("Failed to delete cookie: {}", e);
} else {
println!("Cookie delete attempted");
}
}
CookieAction::Clear => {
if let Err(e) = engine.clear_cookies() {
eprintln!("Failed to clear cookies: {}", e);
} else {
println!("Cookies cleared");
}
}
}
let _ = engine.close();
}
Commands::Config { action } => {
let cfg = rfheadless::EngineConfig::default();
match action {
ConfigAction::Show => {
println!("EngineConfig defaults: {:?}", cfg);
}
ConfigAction::SetConcurrency { value } => {
println!("To run with a different stylesheet fetch concurrency, use: `rfheadless run --stylesheet-concurrency {}`\nThis will affect the next run of the engine.", value);
}
ConfigAction::SetPersistent { enabled } => {
println!("To change persistent runtime behavior for a run, pass: `rfheadless run --enable-persistent-runtime {}`\nThis will affect the next run of the engine.", enabled);
}
}
}
}
Ok(())
}
fn main() {
if std::env::args().nth(1).as_deref() == Some("--worker") {
if let Err(e) = worker_main() {
eprintln!("Worker failed: {}", e);
std::process::exit(1);
}
return;
}
let cli = Cli::parse();
if let Err(e) = run_cli_cmd(cli.command) {
eprintln!("Command failed: {}", e);
std::process::exit(1);
}
}