1use std::time::Duration;
24
25use anyhow::Result;
26use serde_json::Value;
27
28use crate::cli::env_resolver::Source;
29use crate::cli::route;
30use crate::cli::trace::CommandTrace;
31use crate::session::backend::open_backend;
32use crate::session::freshness;
33use crate::session::{with_scratch_recovery, PageSession};
34
35pub async fn run(
36 browser: Option<String>,
37 expression: String,
38 target: Option<String>,
39 json: bool,
40 await_promise: bool,
41 timeout_ms: u64,
42 max_age: String,
43) -> Result<()> {
44 let mut trace = CommandTrace::new("eval");
45 let result = run_inner(
46 browser,
47 expression,
48 target,
49 json,
50 await_promise,
51 timeout_ms,
52 max_age,
53 &mut trace,
54 )
55 .await;
56 trace.finish(result)
57}
58
59#[allow(clippy::too_many_arguments)]
60async fn run_inner(
61 browser: Option<String>,
62 expression: String,
63 target: Option<String>,
64 json: bool,
65 await_promise: bool,
66 timeout_ms: u64,
67 max_age: String,
68 trace: &mut CommandTrace,
69) -> Result<()> {
70 let timeout = Duration::from_millis(timeout_ms);
71 let max_age = freshness::parse_max_age(&max_age)?;
72
73 let r = route::preamble(browser, target.as_deref(), trace).await?;
74 let resolved = &r.resolved;
75
76 let value = match (r.tab_name.clone(), target) {
77 (Some(name), None) => {
80 trace.route("named-tab").tab_name(&name);
81 let expr = expression.clone();
82 route::run_named_tab(
83 &r,
84 &name,
85 "named tabs (`<browser>/<name>`) require a registered browser; \
86 external endpoints can't carry tab names",
87 move |b, target_id| {
88 let expr = expr.clone();
89 async move {
90 b.ensure_fresh(&target_id, max_age).await?;
91 b.evaluate(&target_id, &expr, await_promise, timeout).await
92 }
93 },
94 )
95 .await?
96 }
97 (None, None) => {
100 if matches!(resolved.source, Source::External) {
101 trace.route("direct");
105 let session =
106 PageSession::attach(&resolved.endpoint, resolved.engine, None).await?;
107 session.ensure_fresh(max_age).await?;
108 let value = session
109 .evaluate_with_timeout(&expression, await_promise, Some(timeout))
110 .await;
111 session.close().await;
112 value?
113 } else {
114 trace.route("scratch");
115 let browser_name = match &resolved.source {
116 Source::Registered { name } => name.clone(),
117 _ => unreachable!("Source::External branch handled above"),
118 };
119 let backend = open_backend(&resolved.endpoint, resolved.engine).await?;
120 let expr = expression.clone();
121 let value = with_scratch_recovery(
122 &backend,
123 &r.registry,
124 &browser_name,
125 move |b, target_id| {
126 let expr = expr.clone();
127 async move {
128 b.ensure_fresh(&target_id, max_age).await?;
129 b.evaluate(&target_id, &expr, await_promise, timeout).await
130 }
131 },
132 )
133 .await;
134 backend.shutdown().await;
135 value?
136 }
137 }
138 (None, Some(regex)) => {
140 trace.route("target-regex");
141 let session =
142 PageSession::attach(&resolved.endpoint, resolved.engine, Some(®ex)).await?;
143 session.ensure_fresh(max_age).await?;
144 let value = session
145 .evaluate_with_timeout(&expression, await_promise, Some(timeout))
146 .await;
147 session.close().await;
148 value?
149 }
150 _ => unreachable!("mutex was checked above"),
151 };
152
153 println!("{}", format_output(&value, json));
154 Ok(())
155}
156
157fn format_output(v: &Value, json: bool) -> String {
158 if json {
159 serde_json::to_string_pretty(v).unwrap_or_else(|_| v.to_string())
160 } else if let Some(s) = v.as_str() {
161 s.to_string()
162 } else {
163 serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use serde_json::json;
171
172 #[test]
173 fn eval_returns_string_unquoted_in_text_mode() {
174 let v = json!("hello");
175 assert_eq!(format_output(&v, false), "hello");
176 }
177
178 #[test]
179 fn eval_returns_number_as_json() {
180 let v = json!(42);
181 assert_eq!(format_output(&v, false), "42");
182 }
183
184 #[test]
185 fn eval_returns_json_envelope_when_json_flag() {
186 let v = json!({"a": 1});
187 let out = format_output(&v, true);
188 let parsed: Value = serde_json::from_str(&out).expect("valid JSON");
189 assert_eq!(parsed, v);
190 assert!(out.contains("\"a\""));
191 }
192
193 #[test]
194 fn eval_null_text_mode() {
195 let v = json!(null);
196 assert_eq!(format_output(&v, false), "null");
197 }
198
199 #[test]
200 fn eval_bool_text_mode() {
201 assert_eq!(format_output(&json!(true), false), "true");
202 }
203
204 use crate::detect::Engine;
205 use crate::session::PageSession;
206 use futures_util::{SinkExt, StreamExt};
207 use tokio_tungstenite::tungstenite::Message;
208
209 async fn spawn_cdp_mock(targets: Vec<Value>, eval_value: Value) -> String {
210 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
211 let addr = listener.local_addr().unwrap();
212 tokio::spawn(async move {
213 let (stream, _) = listener.accept().await.unwrap();
214 let mut ws = tokio_tungstenite::accept_async(stream).await.unwrap();
215 while let Some(Ok(Message::Text(t))) = ws.next().await {
216 let req: Value = serde_json::from_str(&t).unwrap();
217 let id = req["id"].as_u64().unwrap();
218 let method = req["method"].as_str().unwrap_or("");
219 let result = match method {
220 "Target.getTargets" => json!({"targetInfos": targets.clone()}),
221 "Target.attachToTarget" => json!({"sessionId": "S1"}),
222 "Runtime.evaluate" => json!({"result": {"value": eval_value.clone()}}),
223 _ => json!({}),
224 };
225 let resp = json!({"id": id, "result": result});
226 ws.send(Message::Text(resp.to_string())).await.unwrap();
227 }
228 });
229 format!("ws://{addr}")
230 }
231
232 #[tokio::test]
233 async fn eval_mock_returns_string() {
234 let url = spawn_cdp_mock(
235 vec![json!({"targetId":"a","type":"page","url":"https://example.com/"})],
236 json!("hello"),
237 )
238 .await;
239 let s = PageSession::attach(&url, Engine::Cdp, None).await.unwrap();
240 let v = s.evaluate("'hello'", false).await.unwrap();
241 s.close().await;
242 assert_eq!(format_output(&v, false), "hello");
243 }
244
245 #[tokio::test]
246 async fn eval_mock_returns_object_json() {
247 let url = spawn_cdp_mock(
248 vec![json!({"targetId":"a","type":"page","url":"https://example.com/"})],
249 json!({"a": 1}),
250 )
251 .await;
252 let s = PageSession::attach(&url, Engine::Cdp, None).await.unwrap();
253 let v = s.evaluate("({a:1})", false).await.unwrap();
254 s.close().await;
255 let out = format_output(&v, true);
256 let parsed: Value = serde_json::from_str(&out).unwrap();
257 assert_eq!(parsed, json!({"a": 1}));
258 }
259}