Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
760 changes: 735 additions & 25 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion crates/ark/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[package]
name = "ark"
version = "0.1.11"
version = "0.1.12"
edition = "2021"
rust-version = "1.70.0"
description = """
The Amalthea R Kernel.
"""

[dependencies]
actix-web = "4.4.0"
amalthea = { path = "../amalthea" }
anyhow = { version = "^1.0", features = ["backtrace"] }
async-trait = "0.1.66"
Expand All @@ -33,7 +34,9 @@ notify = "6.0.0"
once_cell = "1.17.1"
parking_lot = "0.12.1"
regex = "1.7.1"
reqwest = { version = "0.11.20", features = ["json"] }
ropey = "1.6.0"
rust-embed = "8.0.0"
scraper = "0.15.0"
serde = { version = "1.0.183", features = ["derive"] }
serde_json = "1.0.94"
Expand All @@ -43,6 +46,7 @@ tower-lsp = "0.19.0"
tree-sitter = "0.20.9"
tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", branch = "next" }
uuid = "1.3.0"
url = "2.4.1"
walkdir = "2"
yaml-rust = "0.4.5"

Expand Down
2 changes: 1 addition & 1 deletion crates/ark/src/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub unsafe extern "C" fn ps_browse_url(url: SEXP) -> SEXP {

unsafe fn handle_help_url(url: &str) -> Result<bool> {
// Check for help URLs
let port = RFunction::new("tools", "httpdPort").call()?.to::<i32>()?;
let port = RFunction::new("tools", "httpdPort").call()?.to::<u16>()?;
let prefix = format!("http://127.0.0.1:{}/", port);
if !url.starts_with(&prefix) {
return Ok(false);
Expand Down
189 changes: 138 additions & 51 deletions crates/ark/src/help_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,72 +5,159 @@
//
//

use std::net::Ipv4Addr;
use std::net::SocketAddr;

use http::*;
use hyper::client::conn::handshake;
use hyper::server::conn::Http;
use hyper::service::service_fn;
use hyper::Body;
use std::net::TcpListener;

use actix_web::get;
use actix_web::web;
use actix_web::App;
use actix_web::HttpResponse;
use actix_web::HttpServer;
use rust_embed::RustEmbed;
use stdext::spawn;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
use url::Url;

use crate::browser;

async fn handle_request(request: Request<Body>, port: i32) -> anyhow::Result<Response<Body>> {
// connect to R help server
let addr = format!("localhost:{}", port);
let stream = TcpStream::connect(addr.as_str()).await?;
let (mut sender, conn) = handshake(stream).await?;
// Embed src/resources which is where replacement resources can be found.
#[derive(RustEmbed)]
#[folder = "src/resources/"]
struct Asset;

// spawn a task to poll the connection and drive the HTTP state
tokio::spawn(async move {
if let Err(error) = conn.await {
log::error!("HELP PROXY ERROR: {}", error);
// Starts the help proxy.
pub fn start(target_port: u16) {
spawn!("ark-help-proxy", move || {
match task(target_port) {
Ok(value) => log::info!("Help proxy server exited with value: {:?}", value),
Err(error) => log::error!("Help proxy server exited unexpectedly: {}", error),
}
});
}

// send the request
let response = sender.send_request(request).await?;
// The help proxy main entry point.
#[tokio::main]
async fn task(target_port: u16) -> anyhow::Result<()> {
// Create the help proxy.
let help_proxy = HelpProxy::new(target_port)?;

// forward the response
Ok(response)
// Set the help proxy port.
unsafe { browser::PORT = help_proxy.source_port };

// Run the help proxy.
Ok(help_proxy.run().await?)
}

#[tokio::main]
async fn task(port: i32) -> anyhow::Result<()> {
let addr = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
let listener = TcpListener::bind(addr).await?;

if let Ok(addr) = listener.local_addr() {
let port = addr.port();
log::info!("Help proxy listening on port {}", port);
unsafe { browser::PORT = port };
}
// AppState struct.
#[derive(Clone)]
struct AppState {
pub target_port: u16,
}

loop {
let (stream, _) = listener.accept().await?;
tokio::spawn(async move {
let http = Http::new();
let status = http.serve_connection(
stream,
service_fn(|request| async move { handle_request(request, port).await }),
);
// HelpProxy struct.
struct HelpProxy {
pub source_port: u16,
pub target_port: u16,
}

if let Err(error) = status.await {
log::error!("HELP PROXY ERROR: {}", error);
}
// HelpProxy implementation.
impl HelpProxy {
// Creates a new HelpProxy.
pub fn new(target_port: u16) -> anyhow::Result<Self> {
Ok(HelpProxy {
source_port: TcpListener::bind("127.0.0.1:0")?.local_addr()?.port(),
target_port,
})
}

// Runs the HelpProxy.
pub async fn run(&self) -> anyhow::Result<()> {
// Create the app state.
let app_state = web::Data::new(AppState {
target_port: self.target_port,
});

// Create the server.
let server = HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.service(proxy_request)
})
.bind(("127.0.0.1", self.source_port))?;

// Run the server.
Ok(server.run().await?)
}
}

pub fn start(port: i32) {
spawn!("ark-help-proxy", move || {
match task(port) {
Ok(value) => log::info!("Help proxy server exited with value {:?}", value),
Err(error) => log::error!("Help proxy server exited unexpectedly: {}", error),
}
});
// Proxies a request.
#[get("/{url:.*}")]
async fn proxy_request(path: web::Path<(String,)>, app_state: web::Data<AppState>) -> HttpResponse {
// Get the URL path.
let (path,) = path.into_inner();

// Construct the target URL string.
let target_url_string = format!("http://localhost:{}/{path}", app_state.target_port);

// Parse the target URL string into the target URL.
let target_url = match Url::parse(&target_url_string) {
Ok(url) => url,
Err(error) => {
log::error!("Error proxying {}: {}", target_url_string, error);
return HttpResponse::BadGateway().finish();
},
};

// Get the target URL.
match reqwest::get(target_url.clone()).await {
// OK.
Ok(response) => {
// We only handle OK. Everything else is unexpected.
if response.status() != reqwest::StatusCode::OK {
return HttpResponse::BadGateway().finish();
}

// Get the headers we need.
let headers = response.headers().clone();
let content_type = headers.get("content-type");

// Log.
log::info!(
"Proxing URL '{:?}' path '{}' content-type is '{:?}'",
target_url.to_string(),
target_url.path(),
content_type,
);

// Build and return the response.
let mut http_response_builder = HttpResponse::Ok();
if content_type.is_some() {
http_response_builder.content_type(content_type.unwrap());
}

// Certain resources are replaced.
let replacement_embedded_file = match target_url.path().to_lowercase() {
path if path.ends_with("r.css") => Asset::get("R.css"),
path if path.ends_with("prism.css") => Asset::get("prism.css"),
_ => None,
};

// Return the replacement resource or the real resource.
match replacement_embedded_file {
Some(replacement_embedded_file) => {
http_response_builder.body(replacement_embedded_file.data)
},
None => http_response_builder.body(match response.bytes().await {
Ok(body) => body,
Err(error) => {
log::error!("Error proxying {}: {}", target_url_string, error);
return HttpResponse::BadGateway().finish();
},
}),
}
},
// Error.
Err(error) => {
log::error!("Error proxying {}: {}", target_url, error);
HttpResponse::BadGateway().finish()
},
}
}
9 changes: 4 additions & 5 deletions crates/ark/src/modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static mut POSITRON_ATTACHED_ENVIRONMENT: SEXP = std::ptr::null_mut();
pub const POSITRION_ATTACHED_ENVIRONMENT_NAME: &str = "tools:positron";

pub struct RModuleInfo {
pub help_server_port: i32,
pub help_server_port: u16,
}

// NOTE: We use a custom watcher implementation here to detect changes
Expand Down Expand Up @@ -205,10 +205,9 @@ pub unsafe fn initialize() -> anyhow::Result<RModuleInfo> {
}
});

// Get the help server port.
let help_server_port = RFunction::new("tools", "httpdPort").call()?.to::<i32>()?;

return Ok(RModuleInfo { help_server_port });
return Ok(RModuleInfo {
help_server_port: RFunction::new("tools", "httpdPort").call()?.to::<u16>()?,
});
}

pub unsafe fn import(file: &Path) -> anyhow::Result<()> {
Expand Down
90 changes: 90 additions & 0 deletions crates/ark/src/resources/R.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* R.css
*
* Copyright (C) 2023 Posit Software, PBC. All rights reserved.
*
*/

body, td {
font-size: var(--vscode-font-size);
font-family: var(--vscode-font-family);
color: var(--vscode-editor-foreground);
background: var(--vscode-editor-background);
line-height: 1.5;
}
body code,
body pre {
color: var(--vscode-editor-foreground);
font-size: var(--vscode-editor-font-size);
font-family: var(--vscode-editor-font-family);
font-weight: var(--vscode-editor-font-weight);
}
a {
color: var(--vscode-textLink-foreground);
}
::selection {
background: var(--vscode-editor-selectionBackground);
}
h1 {
font-size: x-large;
}
h2 {
font-size: x-large;
font-weight: normal;
}
h3 {
}
h4 {
font-style: italic;
}
h5 {
}
h6 {
font-style: italic;
}
img.toplogo {
max-width: 4em;
vertical-align: middle;
}
img.arrow {
width: 30px;
height: 30px;
border: 0;
}
span.acronym {
font-size: small;
}
span.env {
font-size: var(--vscode-editor-font-size);
font-family: var(--vscode-editor-font-family);
}
span.file {
font-size: var(--vscode-editor-font-size);
font-family: var(--vscode-editor-font-family);
}
span.option {
font-size: var(--vscode-editor-font-size);
font-family: var(--vscode-editor-font-family);
}
span.pkg {
font-weight: bold;
}
span.samp {
font-size: var(--vscode-editor-font-size);
font-family: var(--vscode-editor-font-family);
}
table p {
margin-top: 0;
margin-bottom: 6px;
margin-left: 6px;
}
h3.r-arguments-title + table tr td:first-child {
vertical-align: top;
min-width: 24px;
padding-right: 12px;
}
hr {
height: 1.5px;
border: none;
background-color: var(--vscode-textBlockQuote-border);
}
Loading