diff options
author | Zander Hill <zander@xargs.io> | 2024-07-04 15:12:14 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-04 22:12:14 +0000 |
commit | f00f0f92983d6966a5b97e539ec3f3407c3d851f (patch) | |
tree | 73966bbfbd836dd3dd36ff22647c97d0f839baed | |
parent | 96b527b8df3c9e7e29c98a6a0d6876089b88bc09 (diff) |
feat(jupyter): support `confirm` and `prompt` in notebooks (#23592)
Closes: https://github.com/denoland/deno/issues/22633
This commit adds support for `confirm` and `prompt` APIs,
that instead of reading from stdin are using notebook frontend
to show modal boxes and wait for answers.
---------
Co-authored-by: Bartek IwaĆczuk <biwanczuk@gmail.com>
-rw-r--r-- | cli/js/40_jupyter.js | 48 | ||||
-rw-r--r-- | cli/ops/jupyter.rs | 73 | ||||
-rw-r--r-- | cli/tools/jupyter/mod.rs | 1 | ||||
-rw-r--r-- | cli/tools/jupyter/server.rs | 55 | ||||
-rw-r--r-- | runtime/js/99_main.js | 1 |
5 files changed, 165 insertions, 13 deletions
diff --git a/cli/js/40_jupyter.js b/cli/js/40_jupyter.js index 0e0a4d7ac..ace50d6dc 100644 --- a/cli/js/40_jupyter.js +++ b/cli/js/40_jupyter.js @@ -337,7 +337,14 @@ async function formatInner(obj, raw) { internals.jupyter = { formatInner }; function enableJupyter() { - const { op_jupyter_broadcast } = core.ops; + const { op_jupyter_broadcast, op_jupyter_input } = core.ops; + + function input( + prompt, + password, + ) { + return op_jupyter_input(prompt, password); + } async function broadcast( msgType, @@ -412,6 +419,45 @@ function enableJupyter() { return; } + /** + * Prompt for user confirmation (in Jupyter Notebook context) + * Override confirm and prompt because they depend on a tty + * and in the Deno.jupyter environment that doesn't exist. + * @param {string} message - The message to display. + * @returns {Promise<boolean>} User confirmation. + */ + function confirm(message = "Confirm") { + const answer = input(`${message} [y/N] `, false); + return answer === "Y" || answer === "y"; + } + + /** + * Prompt for user input (in Jupyter Notebook context) + * @param {string} message - The message to display. + * @param {string} defaultValue - The value used if none is provided. + * @param {object} options Options + * @param {boolean} options.password Hide the output characters + * @returns {Promise<string>} The user input. + */ + function prompt( + message = "Prompt", + defaultValue = "", + { password = false } = {}, + ) { + if (defaultValue != "") { + message += ` [${defaultValue}]`; + } + const answer = input(`${message}`, password); + + if (answer === "") { + return defaultValue; + } + + return answer; + } + + globalThis.confirm = confirm; + globalThis.prompt = prompt; globalThis.Deno.jupyter = { broadcast, display, diff --git a/cli/ops/jupyter.rs b/cli/ops/jupyter.rs index 5a16caf54..95d232f11 100644 --- a/cli/ops/jupyter.rs +++ b/cli/ops/jupyter.rs @@ -1,9 +1,14 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// NOTE(bartlomieju): unfortunately it appears that clippy is broken +// and can't allow a single line ignore for `await_holding_lock`. +#![allow(clippy::await_holding_lock)] + use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; +use jupyter_runtime::InputRequest; use jupyter_runtime::JupyterMessage; use jupyter_runtime::JupyterMessageContent; use jupyter_runtime::KernelIoPubConnection; @@ -11,14 +16,17 @@ use jupyter_runtime::StreamContent; use deno_core::error::AnyError; use deno_core::op2; +use deno_core::parking_lot::Mutex; use deno_core::serde_json; use deno_core::OpState; use tokio::sync::mpsc; -use tokio::sync::Mutex; + +use crate::tools::jupyter::server::StdinConnectionProxy; deno_core::extension!(deno_jupyter, ops = [ op_jupyter_broadcast, + op_jupyter_input, ], options = { sender: mpsc::UnboundedSender<StreamContent>, @@ -32,6 +40,63 @@ deno_core::extension!(deno_jupyter, }, ); +#[op2] +#[string] +pub fn op_jupyter_input( + state: &mut OpState, + #[string] prompt: String, + is_password: bool, +) -> Result<Option<String>, AnyError> { + let (last_execution_request, stdin_connection_proxy) = { + ( + state.borrow::<Arc<Mutex<Option<JupyterMessage>>>>().clone(), + state.borrow::<Arc<Mutex<StdinConnectionProxy>>>().clone(), + ) + }; + + let maybe_last_request = last_execution_request.lock().clone(); + if let Some(last_request) = maybe_last_request { + let JupyterMessageContent::ExecuteRequest(msg) = &last_request.content + else { + return Ok(None); + }; + + if !msg.allow_stdin { + return Ok(None); + } + + let msg = JupyterMessage::new( + InputRequest { + prompt, + password: is_password, + } + .into(), + Some(&last_request), + ); + + let Ok(()) = stdin_connection_proxy.lock().tx.send(msg) else { + return Ok(None); + }; + + // Need to spawn a separate thread here, because `blocking_recv()` can't + // be used from the Tokio runtime context. + let join_handle = std::thread::spawn(move || { + stdin_connection_proxy.lock().rx.blocking_recv() + }); + let Ok(Some(response)) = join_handle.join() else { + return Ok(None); + }; + + let JupyterMessageContent::InputReply(msg) = response.content else { + return Ok(None); + }; + + return Ok(Some(msg.value)); + } + + Ok(None) +} + #[op2(async)] pub async fn op_jupyter_broadcast( state: Rc<RefCell<OpState>>, @@ -49,7 +114,7 @@ pub async fn op_jupyter_broadcast( ) }; - let maybe_last_request = last_execution_request.lock().await.clone(); + let maybe_last_request = last_execution_request.lock().clone(); if let Some(last_request) = maybe_last_request { let content = JupyterMessageContent::from_type_and_content( &message_type, @@ -69,9 +134,7 @@ pub async fn op_jupyter_broadcast( .with_metadata(metadata) .with_buffers(buffers.into_iter().map(|b| b.to_vec().into()).collect()); - (iopub_connection.lock().await) - .send(jupyter_message) - .await?; + iopub_connection.lock().send(jupyter_message).await?; } Ok(()) diff --git a/cli/tools/jupyter/mod.rs b/cli/tools/jupyter/mod.rs index a5139044f..1197d3df7 100644 --- a/cli/tools/jupyter/mod.rs +++ b/cli/tools/jupyter/mod.rs @@ -179,6 +179,7 @@ pub async fn kernel( let mut op_state = op_state_rc.borrow_mut(); op_state.put(startup_data.iopub_connection.clone()); op_state.put(startup_data.last_execution_request.clone()); + op_state.put(startup_data.stdin_connection_proxy.clone()); } repl_session_proxy.start().await; diff --git a/cli/tools/jupyter/server.rs b/cli/tools/jupyter/server.rs index 6a3831c49..6e203d17d 100644 --- a/cli/tools/jupyter/server.rs +++ b/cli/tools/jupyter/server.rs @@ -3,6 +3,10 @@ // This file is forked/ported from <https://github.com/evcxr/evcxr> // Copyright 2020 The Evcxr Authors. MIT license. +// NOTE(bartlomieju): unfortunately it appears that clippy is broken +// and can't allow a single line ignore for `await_holding_lock`. +#![allow(clippy::await_holding_lock)] + use std::collections::HashMap; use std::rc::Rc; use std::sync::Arc; @@ -12,12 +16,12 @@ use crate::tools::repl; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::futures; +use deno_core::parking_lot::Mutex; use deno_core::serde_json; use deno_core::CancelFuture; use deno_core::CancelHandle; use tokio::sync::mpsc; use tokio::sync::oneshot; -use tokio::sync::Mutex; use jupyter_runtime::messaging; use jupyter_runtime::AsChildOf; @@ -40,8 +44,14 @@ pub struct JupyterServer { repl_session_proxy: JupyterReplProxy, } +pub struct StdinConnectionProxy { + pub tx: mpsc::UnboundedSender<JupyterMessage>, + pub rx: mpsc::UnboundedReceiver<JupyterMessage>, +} + pub struct StartupData { pub iopub_connection: Arc<Mutex<KernelIoPubConnection>>, + pub stdin_connection_proxy: Arc<Mutex<StdinConnectionProxy>>, pub last_execution_request: Arc<Mutex<Option<JupyterMessage>>>, } @@ -58,7 +68,7 @@ impl JupyterServer { connection_info.create_kernel_shell_connection().await?; let control_connection = connection_info.create_kernel_control_connection().await?; - let _stdin_connection = + let mut stdin_connection = connection_info.create_kernel_stdin_connection().await?; let iopub_connection = connection_info.create_kernel_iopub_connection().await?; @@ -66,9 +76,19 @@ impl JupyterServer { let iopub_connection = Arc::new(Mutex::new(iopub_connection)); let last_execution_request = Arc::new(Mutex::new(None)); + let (stdin_tx1, mut stdin_rx1) = + mpsc::unbounded_channel::<JupyterMessage>(); + let (stdin_tx2, stdin_rx2) = mpsc::unbounded_channel::<JupyterMessage>(); + + let stdin_connection_proxy = Arc::new(Mutex::new(StdinConnectionProxy { + tx: stdin_tx1, + rx: stdin_rx2, + })); + let Ok(()) = setup_tx.send(StartupData { iopub_connection: iopub_connection.clone(), last_execution_request: last_execution_request.clone(), + stdin_connection_proxy, }) else { bail!("Failed to send startup data"); }; @@ -82,6 +102,24 @@ impl JupyterServer { repl_session_proxy, }; + let stdin_fut = deno_core::unsync::spawn(async move { + loop { + let Some(msg) = stdin_rx1.recv().await else { + return; + }; + let Ok(()) = stdin_connection.send(msg).await else { + return; + }; + + let Ok(msg) = stdin_connection.read().await else { + return; + }; + let Ok(()) = stdin_tx2.send(msg) else { + return; + }; + } + }); + let hearbeat_fut = deno_core::unsync::spawn(async move { loop { if let Err(err) = heartbeat.single_heartbeat().await { @@ -134,6 +172,7 @@ impl JupyterServer { shell_fut, stdio_fut, repl_session_fut, + stdin_fut, ]); if let Ok(result) = join_fut.or_cancel(cancel_handle).await { @@ -148,13 +187,15 @@ impl JupyterServer { last_execution_request: Arc<Mutex<Option<JupyterMessage>>>, stdio_msg: StreamContent, ) { - let maybe_exec_result = last_execution_request.lock().await.clone(); + let maybe_exec_result = last_execution_request.lock().clone(); let Some(exec_request) = maybe_exec_result else { return; }; - let mut iopub_conn = iopub_connection.lock().await; - let result = iopub_conn.send(stdio_msg.as_child_of(&exec_request)).await; + let result = iopub_connection + .lock() + .send(stdio_msg.as_child_of(&exec_request)) + .await; if let Err(err) = result { log::error!("Output error: {}", err); @@ -429,7 +470,7 @@ impl JupyterServer { if !execute_request.silent && execute_request.store_history { self.execution_count += 1; } - *self.last_execution_request.lock().await = Some(parent_message.clone()); + *self.last_execution_request.lock() = Some(parent_message.clone()); self .send_iopub( @@ -613,7 +654,7 @@ impl JupyterServer { &mut self, message: JupyterMessage, ) -> Result<(), AnyError> { - self.iopub_connection.lock().await.send(message).await + self.iopub_connection.lock().send(message).await } } diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 0a750f891..44dc3c54d 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -579,6 +579,7 @@ const NOT_IMPORTED_OPS = [ // Related to `Deno.jupyter` API "op_jupyter_broadcast", + "op_jupyter_input", // Related to `Deno.test()` API "op_test_event_step_result_failed", |