//! Durable V8 harness, host-side SQLite storage, and world tool operations. mod credentials; mod environment; use crate::{exec::Exec, world::World}; use deno_core::{JsRuntime, OpState, RuntimeOptions, op2}; use deno_error::JsErrorBox; use rusqlite::{Connection, types::Value as SqlValue}; use serde_json::{Value, json}; use std::path::Path; use std::sync::Arc; #[op2] #[serde] fn op_sql( state: &mut OpState, #[string] kind: String, #[string] sql: String, #[serde] params: Vec, ) -> Result { let work = || -> Result> { if kind == "open" { let db = Connection::open(sql)?; db.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;")?; state.put(db); return Ok(Value::Null); } if kind == "close" { state.take::(); return Ok(Value::Null); } let db = state.borrow::(); if kind == "exec" { db.execute_batch(&sql)?; return Ok(Value::Null); } let params: Vec = params .into_iter() .map(|v| match v { Value::Null => SqlValue::Null, Value::Number(n) => n .as_i64() .map(SqlValue::Integer) .unwrap_or_else(|| SqlValue::Real(n.as_f64().unwrap())), Value::String(s) => SqlValue::Text(s), Value::Array(a) => { SqlValue::Blob(a.into_iter().map(|n| n.as_u64().unwrap() as u8).collect()) } _ => panic!("unsupported SQL parameter"), }) .collect(); let mut stmt = db.prepare_cached(&sql)?; if kind == "run" { let changes = stmt.execute(rusqlite::params_from_iter(params))?; return Ok(json!({"changes": changes, "lastInsertRowid": db.last_insert_rowid()})); } let names: Vec = stmt.column_names().into_iter().map(str::to_owned).collect(); let mut rows = stmt.query(rusqlite::params_from_iter(params))?; let mut result = vec![]; while let Some(row) = rows.next()? { let mut obj = serde_json::Map::new(); for (i, name) in names.iter().enumerate() { let v = match row.get::<_, SqlValue>(i)? { SqlValue::Null => Value::Null, SqlValue::Integer(n) => json!(n), SqlValue::Real(n) => json!(n), SqlValue::Text(s) => json!(s), SqlValue::Blob(b) => json!(b), }; obj.insert(name.clone(), v); } result.push(Value::Object(obj)); } Ok(json!(result)) }; work().map_err(|e| JsErrorBox::generic(e.to_string())) } deno_core::extension!( storage, ops = [ op_sql, credentials::op_credential_requests, credentials::op_model_proxy, credentials::op_credential_next, environment::op_file, environment::op_exec, environment::op_exec_next, environment::op_input, environment::op_output ] ); /// Start the console with the process implementation supplied by the host. pub async fn run_with_exec(world: World, exec: Arc) -> anyhow::Result<()> { let cred = Arc::new(crate::cred::Cred::open(&world.host_dir)?); run_with_shared_credentials(world, exec, cred).await } async fn run_with_shared_credentials( world: World, exec: Arc, cred: Arc, ) -> anyhow::Result<()> { // Deno's unsynchronized operations require a current-thread Tokio runtime. tokio::task::spawn_blocking(move || { tokio::runtime::Builder::new_current_thread() .enable_all() .build()? .block_on(run_module_with_credentials( world, exec, cred, include_str!("../../js/bundle.js"), )) }) .await? .map_err(|error: anyhow::Error| { let mut text = error.to_string(); for key in ["PI_PROXY_BASE_URL", "PI_PROXY_API_KEY"] { if let Ok(value) = std::env::var(key) && !value.is_empty() { text = text.replace(&value, "[redacted]"); } } anyhow::anyhow!(text) }) } /// Evaluate a bundled harness module with the same storage and tool operations as the console. /// The caller must use a current-thread Tokio runtime. pub async fn run_module(world: World, exec: Arc, source: &str) -> anyhow::Result<()> { let cred = Arc::new(crate::cred::store::Cred::open(&world.host_dir)?); run_module_with_credentials(world, exec, cred, source).await } /// Run a harness with the credentials handle shared by the host API. pub async fn run_module_with_credentials( world: World, exec: Arc, cred: Arc, source: &str, ) -> anyhow::Result<()> { let proxy = credentials::proxy(&cred)?; let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![ deno_telemetry::deno_telemetry::init(), deno_webidl::deno_webidl::init(), deno_web::deno_web::init( Arc::new(deno_web::BlobStore::default()), None, false, Default::default(), ), deno_crypto::deno_crypto::init(None), deno_net::deno_net::init(None, None), deno_fetch::deno_fetch::init(Default::default()), storage::init(), ], extension_transpiler: Some(std::rc::Rc::new(|name, source| { if !name.ends_with(".ts") { return Ok((source, None)); } let parsed = deno_ast::parse_module(deno_ast::ParseParams { specifier: deno_core::url::Url::parse(&name).unwrap(), text: source.into(), media_type: deno_ast::MediaType::TypeScript, capture_tokens: false, scope_analysis: false, maybe_syntax: None, }) .map_err(|e| JsErrorBox::generic(e.to_string()))?; let emitted = parsed .transpile( &Default::default(), &Default::default(), &deno_ast::EmitOptions { source_map: deno_ast::SourceMapOption::None, ..Default::default() }, ) .map_err(|e| JsErrorBox::generic(e.to_string()))? .into_source(); Ok((emitted.text.into(), None)) })), ..Default::default() }); runtime .op_state() .borrow_mut() .put(deno_permissions::PermissionsContainer::allow_all(Arc::new( deno_permissions::RuntimePermissionDescriptorParser::new(sys_traits::impls::RealSys), ))); runtime.execute_script("bootstrap.js", include_str!("../../js/bootstrap.js"))?; runtime .op_state() .borrow_mut() .put(credentials::Notifications::new(&cred)); runtime.op_state().borrow_mut().put(cred); runtime .op_state() .borrow_mut() .put(environment::Environment { root: world.root, exec, }); runtime.execute_script( "config.js", format!( "globalThis.storagePath = {};", serde_json::to_string(&world.host_dir.join("harness.sqlite"))? ), )?; let specifier = deno_core::ModuleSpecifier::parse("file:///harness.js")?; let module = runtime .load_main_es_module_from_code(&specifier, source.to_owned()) .await?; let result = runtime.mod_evaluate(module); let result = async { runtime .with_event_loop_promise(result, Default::default()) .await?; runtime.run_event_loop(Default::default()).await?; Ok::<_, anyhow::Error>(()) } .await; result.map_err(|error| { let mut text = error.to_string(); if let Some(key) = proxy .as_ref() .and_then(|v| v.get("api_key")) .and_then(Value::as_str) && !key.is_empty() { text = text.replace(key, "[redacted]"); } anyhow::anyhow!(text) }) } /// Open a world and resume its durable root conversation. pub async fn run(path: &Path) -> anyhow::Result<()> { run_with_userland(path, None).await } /// Start the console with the shared process table and an optional userland bundle. pub async fn run_with_userland(path: &Path, userland: Option<&Path>) -> anyhow::Result<()> { let world = World::open_with_userland(path, userland)?; let cred = Arc::new(crate::cred::Cred::open(&world.host_dir)?); let runtime = crate::process::Runtime::new_with_hooks( World { root: world.root.clone(), host_dir: world.host_dir.clone(), processes: world.processes.clone(), }, cred.clone(), crate::process::default_script(), None, )?; let services = crate::process::supervise(runtime.clone()); let result = run_with_shared_credentials(world, Arc::new(runtime), cred).await; services.shutdown().await; result }