Skip to content

Commit 3663460

Browse files
Split inputs by newlines (#536)
* Add `lines()` util to preserve trailing lines * Draft `handle_input()` * WIP handling for pending line in `read_console()` * Fix output of intermediate expressions * Make detection of prompt types more robust * Rework `lines()` to return an iterator * Flesh out safety bullet * Clean up `buffer_overflow_call()` * Remove comment * Update comment about incomplete input handling * Throw an R error if we see incomplete user inputs Should only happen in Jupyter Notebooks. Positron should always check for complete inputs before sending them through. * Add a note about input replies and incomplete results * Add more comments about input handling * Return error from `check_console_input()` * Document the ReadConsole state machine * Panic when an incomplete prompt is detected in `ReadConsole` * Document current issue with large input errors * Tweak tests * Set `R_CLI_HIDE_CURSOR` after all * Return an `amalthea::Result<()>` from `on_console_input()` Allowing us to manually throw an R error from the POD site * We are happy with this being an error * Update some documentation * Clean up long single line test * Add basic stdin test, and stdin buffer overflow test * Turn off stack limit on Windows too And add tests that its actually off during harp/ark `r_task()` tests and frontend integration tests * Collect IOPub Stream messages in batches * Correct ordering in `report_error!` * Group structs and impls together * Also test for prefix stream * Mention fix in changelog * Add tests for incompete inputs and browser prompts * Improve readline prompt detection in browser sessions posit-dev/positron#4742 * Add test for errors in browser * Update comments * Add test for readline in browser sessions * Move `r_task()` initialization after R was set up --------- Co-authored-by: Lionel Henry <lionel.hry@proton.me> Co-authored-by: Davis Vaughan <davis@posit.co>
1 parent 0a06ddf commit 3663460

File tree

15 files changed

+858
-115
lines changed

15 files changed

+858
-115
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
# 0.1.9000
44

5+
- Sending long inputs of more than 4096 bytes no longer fails (posit-dev/positron#4745).
6+
57
- Jupyter: Fixed a bug in the kernel-info reply where the `pygments_lexer` field
68
would be set incorrectly to `""` (#553).
79

crates/amalthea/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub enum Error {
4343
UnknownCommId(String),
4444
InvalidCommMessage(String, String, String),
4545
InvalidInputRequest(String),
46+
InvalidConsoleInput(String),
4647
}
4748

4849
impl std::error::Error for Error {}
@@ -193,6 +194,9 @@ impl fmt::Display for Error {
193194
Error::InvalidInputRequest(message) => {
194195
write!(f, "{message}")
195196
},
197+
Error::InvalidConsoleInput(message) => {
198+
write!(f, "{message}")
199+
},
196200
}
197201
}
198202
}

crates/amalthea/src/fixtures/dummy_frontend.rs

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::session::Session;
1313
use crate::socket::socket::Socket;
1414
use crate::wire::execute_input::ExecuteInput;
1515
use crate::wire::execute_request::ExecuteRequest;
16+
use crate::wire::input_reply::InputReply;
1617
use crate::wire::jupyter_message::JupyterMessage;
1718
use crate::wire::jupyter_message::Message;
1819
use crate::wire::jupyter_message::ProtocolMessage;
@@ -36,6 +37,10 @@ pub struct DummyFrontend {
3637
heartbeat_port: u16,
3738
}
3839

40+
pub struct ExecuteRequestOptions {
41+
pub allow_stdin: bool,
42+
}
43+
3944
impl DummyFrontend {
4045
pub fn new() -> Self {
4146
use rand::Rng;
@@ -139,13 +144,13 @@ impl DummyFrontend {
139144
id
140145
}
141146

142-
pub fn send_execute_request(&self, code: &str) -> String {
147+
pub fn send_execute_request(&self, code: &str, options: ExecuteRequestOptions) -> String {
143148
self.send_shell(ExecuteRequest {
144149
code: String::from(code),
145150
silent: false,
146151
store_history: true,
147152
user_expressions: serde_json::Value::Null,
148-
allow_stdin: false,
153+
allow_stdin: options.allow_stdin,
149154
stop_on_error: false,
150155
})
151156
}
@@ -248,22 +253,48 @@ impl DummyFrontend {
248253
})
249254
}
250255

251-
pub fn recv_iopub_stream_stdout(&self) -> String {
252-
let msg = self.recv_iopub();
253-
254-
assert_matches!(msg, Message::StreamOutput(data) => {
255-
assert_eq!(data.content.name, Stream::Stdout);
256-
data.content.text
257-
})
256+
pub fn recv_iopub_stream_stdout(&self, expect: &str) {
257+
self.recv_iopub_stream(expect, Stream::Stdout)
258258
}
259259

260-
pub fn recv_iopub_stream_stderr(&self) -> String {
261-
let msg = self.recv_iopub();
260+
pub fn recv_iopub_stream_stderr(&self, expect: &str) {
261+
self.recv_iopub_stream(expect, Stream::Stderr)
262+
}
262263

263-
assert_matches!(msg, Message::StreamOutput(data) => {
264-
assert_eq!(data.content.name, Stream::Stderr);
265-
data.content.text
266-
})
264+
/// Receive from IOPub Stream
265+
///
266+
/// Stdout and Stderr Stream messages are buffered, so to reliably test against them
267+
/// we have to collect the messages in batches on the receiving end and compare against
268+
/// an expected message.
269+
fn recv_iopub_stream(&self, expect: &str, stream: Stream) {
270+
let mut out = String::new();
271+
272+
loop {
273+
// Receive a piece of stream output (with a timeout)
274+
let msg = self.recv_iopub();
275+
276+
// Assert its type
277+
let piece = assert_matches!(msg, Message::StreamOutput(data) => {
278+
assert_eq!(data.content.name, stream);
279+
data.content.text
280+
});
281+
282+
// Add to what we've already collected
283+
out += piece.as_str();
284+
285+
if out == expect {
286+
// Done, found the entire `expect` string
287+
return;
288+
}
289+
290+
if !expect.starts_with(out.as_str()) {
291+
// Something is wrong, message doesn't match up
292+
panic!("Expected IOPub stream of '{expect}'. Actual stream of '{out}'.");
293+
}
294+
295+
// We have a prefix of `expect`, but not the whole message yet.
296+
// Wait on the next IOPub Stream message.
297+
}
267298
}
268299

269300
/// Receive from IOPub and assert ExecuteResult message. Returns compulsory
@@ -276,6 +307,21 @@ impl DummyFrontend {
276307
})
277308
}
278309

310+
/// Receive from Stdin and assert `InputRequest` message.
311+
/// Returns the `prompt`.
312+
pub fn recv_stdin_input_request(&self) -> String {
313+
let msg = self.recv_stdin();
314+
315+
assert_matches!(msg, Message::InputRequest(data) => {
316+
data.content.prompt
317+
})
318+
}
319+
320+
/// Send back an `InputReply` to an `InputRequest` over Stdin
321+
pub fn send_stdin_input_reply(&self, value: String) {
322+
self.send_stdin(InputReply { value })
323+
}
324+
279325
/// Receives a (raw) message from the heartbeat socket
280326
pub fn recv_heartbeat(&self) -> zmq::Message {
281327
let mut msg = zmq::Message::new();
@@ -339,3 +385,9 @@ impl DummyFrontend {
339385
}
340386
}
341387
}
388+
389+
impl Default for ExecuteRequestOptions {
390+
fn default() -> Self {
391+
Self { allow_stdin: false }
392+
}
393+
}

crates/amalthea/src/kernel.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use crate::wire::jupyter_message::Message;
3838
use crate::wire::jupyter_message::OutboundMessage;
3939

4040
macro_rules! report_error {
41-
($($arg:tt)+) => (if cfg!(debug_assertions) { log::error!($($arg)+) } else { panic!($($arg)+) })
41+
($($arg:tt)+) => (if cfg!(debug_assertions) { panic!($($arg)+) } else { log::error!($($arg)+) })
4242
}
4343

4444
/// A Kernel represents a unique Jupyter kernel session and is the host for all

crates/ark/src/analysis/input_boundaries.rs

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,13 @@ impl InputBoundary {
6565
/// an invalid one, and invalid inputs are always trailing).
6666
/// - There is only one incomplete and one invalid input in a set of inputs.
6767
pub fn input_boundaries(text: &str) -> anyhow::Result<Vec<InputBoundary>> {
68-
let mut lines: Vec<&str> = text.lines().collect();
69-
70-
// Rectify for `lines()` ignoring trailing empty lines
71-
match text.chars().last() {
72-
Some(last) if last == '\n' => lines.push(""),
73-
None => lines.push(""),
74-
_ => {},
75-
}
76-
77-
// Create a duplicate vector of lines on the R side too so we don't have to
78-
// reallocate memory each time we parse a new subset of lines
79-
let lines_r = CharacterVector::create(lines.iter());
68+
let lines: Vec<&str> = crate::strings::lines(text).collect();
8069
let n_lines: u32 = lines.len().try_into()?;
8170

71+
// Create the vector on the R side so we don't have to reallocate memory
72+
// for the elements each time we parse a new subset of lines
73+
let lines = CharacterVector::create(lines);
74+
8275
let mut complete: Vec<LineRange> = vec![];
8376
let mut incomplete: Option<LineRange> = None;
8477
let mut invalid: Option<LineRange> = None;
@@ -120,9 +113,9 @@ pub fn input_boundaries(text: &str) -> anyhow::Result<Vec<InputBoundary>> {
120113
// Grab all code up to current line. We don't slice the vector in the
121114
// first iteration as it's not needed.
122115
let subset = if current_line == n_lines - 1 {
123-
lines_r.clone()
116+
lines.clone()
124117
} else {
125-
CharacterVector::try_from(&lines_r.slice()[..=current_line as usize])?
118+
CharacterVector::try_from(&lines.slice()[..=current_line as usize])?
126119
};
127120

128121
// Parse within source file to get source references

crates/ark/src/fixtures/dummy_frontend.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ impl DummyArkFrontend {
5555
let frontend = DummyFrontend::new();
5656
let connection_file = frontend.get_connection_file();
5757

58+
// We don't want cli to try and restore the cursor, it breaks our tests
59+
// by adding unecessary ANSI escapes. We don't need this in Positron because
60+
// cli also checks `isatty(stdout())`, which is false in Positron because
61+
// we redirect stdout.
62+
// https://github.com/r-lib/cli/blob/1220ed092c03e167ff0062e9839c81d7258a4600/R/onload.R#L33-L40
63+
unsafe { std::env::set_var("R_CLI_HIDE_CURSOR", "false") };
64+
5865
// Start the kernel in this thread so that panics are propagated
5966
crate::start::start_kernel(
6067
connection_file,

0 commit comments

Comments
 (0)