Skip to content

Commit 7e14cbd

Browse files
authored
Merge pull request #107 from posit-dev/updates/help-proxy
New help proxy server
2 parents 810d532 + 347af64 commit 7e14cbd

File tree

9 files changed

+1392
-99
lines changed

9 files changed

+1392
-99
lines changed

Cargo.lock

Lines changed: 735 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ark/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
[package]
22
name = "ark"
3-
version = "0.1.11"
3+
version = "0.1.12"
44
edition = "2021"
55
rust-version = "1.70.0"
66
description = """
77
The Amalthea R Kernel.
88
"""
99

1010
[dependencies]
11+
actix-web = "4.4.0"
1112
amalthea = { path = "../amalthea" }
1213
anyhow = { version = "^1.0", features = ["backtrace"] }
1314
async-trait = "0.1.66"
@@ -33,7 +34,9 @@ notify = "6.0.0"
3334
once_cell = "1.17.1"
3435
parking_lot = "0.12.1"
3536
regex = "1.7.1"
37+
reqwest = { version = "0.11.20", features = ["json"] }
3638
ropey = "1.6.0"
39+
rust-embed = "8.0.0"
3740
scraper = "0.15.0"
3841
serde = { version = "1.0.183", features = ["derive"] }
3942
serde_json = "1.0.94"
@@ -43,6 +46,7 @@ tower-lsp = "0.19.0"
4346
tree-sitter = "0.20.9"
4447
tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", branch = "next" }
4548
uuid = "1.3.0"
49+
url = "2.4.1"
4650
walkdir = "2"
4751
yaml-rust = "0.4.5"
4852

crates/ark/src/browser.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub unsafe extern "C" fn ps_browse_url(url: SEXP) -> SEXP {
3131

3232
unsafe fn handle_help_url(url: &str) -> Result<bool> {
3333
// Check for help URLs
34-
let port = RFunction::new("tools", "httpdPort").call()?.to::<i32>()?;
34+
let port = RFunction::new("tools", "httpdPort").call()?.to::<u16>()?;
3535
let prefix = format!("http://127.0.0.1:{}/", port);
3636
if !url.starts_with(&prefix) {
3737
return Ok(false);

crates/ark/src/help_proxy.rs

Lines changed: 138 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,72 +5,159 @@
55
//
66
//
77

8-
use std::net::Ipv4Addr;
9-
use std::net::SocketAddr;
10-
11-
use http::*;
12-
use hyper::client::conn::handshake;
13-
use hyper::server::conn::Http;
14-
use hyper::service::service_fn;
15-
use hyper::Body;
8+
use std::net::TcpListener;
9+
10+
use actix_web::get;
11+
use actix_web::web;
12+
use actix_web::App;
13+
use actix_web::HttpResponse;
14+
use actix_web::HttpServer;
15+
use rust_embed::RustEmbed;
1616
use stdext::spawn;
17-
use tokio::net::TcpListener;
18-
use tokio::net::TcpStream;
17+
use url::Url;
1918

2019
use crate::browser;
2120

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

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

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

38-
// forward the response
39-
Ok(response)
42+
// Set the help proxy port.
43+
unsafe { browser::PORT = help_proxy.source_port };
44+
45+
// Run the help proxy.
46+
Ok(help_proxy.run().await?)
4047
}
4148

42-
#[tokio::main]
43-
async fn task(port: i32) -> anyhow::Result<()> {
44-
let addr = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);
45-
let listener = TcpListener::bind(addr).await?;
46-
47-
if let Ok(addr) = listener.local_addr() {
48-
let port = addr.port();
49-
log::info!("Help proxy listening on port {}", port);
50-
unsafe { browser::PORT = port };
51-
}
49+
// AppState struct.
50+
#[derive(Clone)]
51+
struct AppState {
52+
pub target_port: u16,
53+
}
5254

53-
loop {
54-
let (stream, _) = listener.accept().await?;
55-
tokio::spawn(async move {
56-
let http = Http::new();
57-
let status = http.serve_connection(
58-
stream,
59-
service_fn(|request| async move { handle_request(request, port).await }),
60-
);
55+
// HelpProxy struct.
56+
struct HelpProxy {
57+
pub source_port: u16,
58+
pub target_port: u16,
59+
}
6160

62-
if let Err(error) = status.await {
63-
log::error!("HELP PROXY ERROR: {}", error);
64-
}
61+
// HelpProxy implementation.
62+
impl HelpProxy {
63+
// Creates a new HelpProxy.
64+
pub fn new(target_port: u16) -> anyhow::Result<Self> {
65+
Ok(HelpProxy {
66+
source_port: TcpListener::bind("127.0.0.1:0")?.local_addr()?.port(),
67+
target_port,
68+
})
69+
}
70+
71+
// Runs the HelpProxy.
72+
pub async fn run(&self) -> anyhow::Result<()> {
73+
// Create the app state.
74+
let app_state = web::Data::new(AppState {
75+
target_port: self.target_port,
6576
});
77+
78+
// Create the server.
79+
let server = HttpServer::new(move || {
80+
App::new()
81+
.app_data(app_state.clone())
82+
.service(proxy_request)
83+
})
84+
.bind(("127.0.0.1", self.source_port))?;
85+
86+
// Run the server.
87+
Ok(server.run().await?)
6688
}
6789
}
6890

69-
pub fn start(port: i32) {
70-
spawn!("ark-help-proxy", move || {
71-
match task(port) {
72-
Ok(value) => log::info!("Help proxy server exited with value {:?}", value),
73-
Err(error) => log::error!("Help proxy server exited unexpectedly: {}", error),
74-
}
75-
});
91+
// Proxies a request.
92+
#[get("/{url:.*}")]
93+
async fn proxy_request(path: web::Path<(String,)>, app_state: web::Data<AppState>) -> HttpResponse {
94+
// Get the URL path.
95+
let (path,) = path.into_inner();
96+
97+
// Construct the target URL string.
98+
let target_url_string = format!("http://localhost:{}/{path}", app_state.target_port);
99+
100+
// Parse the target URL string into the target URL.
101+
let target_url = match Url::parse(&target_url_string) {
102+
Ok(url) => url,
103+
Err(error) => {
104+
log::error!("Error proxying {}: {}", target_url_string, error);
105+
return HttpResponse::BadGateway().finish();
106+
},
107+
};
108+
109+
// Get the target URL.
110+
match reqwest::get(target_url.clone()).await {
111+
// OK.
112+
Ok(response) => {
113+
// We only handle OK. Everything else is unexpected.
114+
if response.status() != reqwest::StatusCode::OK {
115+
return HttpResponse::BadGateway().finish();
116+
}
117+
118+
// Get the headers we need.
119+
let headers = response.headers().clone();
120+
let content_type = headers.get("content-type");
121+
122+
// Log.
123+
log::info!(
124+
"Proxing URL '{:?}' path '{}' content-type is '{:?}'",
125+
target_url.to_string(),
126+
target_url.path(),
127+
content_type,
128+
);
129+
130+
// Build and return the response.
131+
let mut http_response_builder = HttpResponse::Ok();
132+
if content_type.is_some() {
133+
http_response_builder.content_type(content_type.unwrap());
134+
}
135+
136+
// Certain resources are replaced.
137+
let replacement_embedded_file = match target_url.path().to_lowercase() {
138+
path if path.ends_with("r.css") => Asset::get("R.css"),
139+
path if path.ends_with("prism.css") => Asset::get("prism.css"),
140+
_ => None,
141+
};
142+
143+
// Return the replacement resource or the real resource.
144+
match replacement_embedded_file {
145+
Some(replacement_embedded_file) => {
146+
http_response_builder.body(replacement_embedded_file.data)
147+
},
148+
None => http_response_builder.body(match response.bytes().await {
149+
Ok(body) => body,
150+
Err(error) => {
151+
log::error!("Error proxying {}: {}", target_url_string, error);
152+
return HttpResponse::BadGateway().finish();
153+
},
154+
}),
155+
}
156+
},
157+
// Error.
158+
Err(error) => {
159+
log::error!("Error proxying {}: {}", target_url, error);
160+
HttpResponse::BadGateway().finish()
161+
},
162+
}
76163
}

crates/ark/src/modules.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ static mut POSITRON_ATTACHED_ENVIRONMENT: SEXP = std::ptr::null_mut();
4343
pub const POSITRION_ATTACHED_ENVIRONMENT_NAME: &str = "tools:positron";
4444

4545
pub struct RModuleInfo {
46-
pub help_server_port: i32,
46+
pub help_server_port: u16,
4747
}
4848

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

208-
// Get the help server port.
209-
let help_server_port = RFunction::new("tools", "httpdPort").call()?.to::<i32>()?;
210-
211-
return Ok(RModuleInfo { help_server_port });
208+
return Ok(RModuleInfo {
209+
help_server_port: RFunction::new("tools", "httpdPort").call()?.to::<u16>()?,
210+
});
212211
}
213212

214213
pub unsafe fn import(file: &Path) -> anyhow::Result<()> {

crates/ark/src/resources/R.css

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* R.css
3+
*
4+
* Copyright (C) 2023 Posit Software, PBC. All rights reserved.
5+
*
6+
*/
7+
8+
body, td {
9+
font-size: var(--vscode-font-size);
10+
font-family: var(--vscode-font-family);
11+
color: var(--vscode-editor-foreground);
12+
background: var(--vscode-editor-background);
13+
line-height: 1.5;
14+
}
15+
body code,
16+
body pre {
17+
color: var(--vscode-editor-foreground);
18+
font-size: var(--vscode-editor-font-size);
19+
font-family: var(--vscode-editor-font-family);
20+
font-weight: var(--vscode-editor-font-weight);
21+
}
22+
a {
23+
color: var(--vscode-textLink-foreground);
24+
}
25+
::selection {
26+
background: var(--vscode-editor-selectionBackground);
27+
}
28+
h1 {
29+
font-size: x-large;
30+
}
31+
h2 {
32+
font-size: x-large;
33+
font-weight: normal;
34+
}
35+
h3 {
36+
}
37+
h4 {
38+
font-style: italic;
39+
}
40+
h5 {
41+
}
42+
h6 {
43+
font-style: italic;
44+
}
45+
img.toplogo {
46+
max-width: 4em;
47+
vertical-align: middle;
48+
}
49+
img.arrow {
50+
width: 30px;
51+
height: 30px;
52+
border: 0;
53+
}
54+
span.acronym {
55+
font-size: small;
56+
}
57+
span.env {
58+
font-size: var(--vscode-editor-font-size);
59+
font-family: var(--vscode-editor-font-family);
60+
}
61+
span.file {
62+
font-size: var(--vscode-editor-font-size);
63+
font-family: var(--vscode-editor-font-family);
64+
}
65+
span.option {
66+
font-size: var(--vscode-editor-font-size);
67+
font-family: var(--vscode-editor-font-family);
68+
}
69+
span.pkg {
70+
font-weight: bold;
71+
}
72+
span.samp {
73+
font-size: var(--vscode-editor-font-size);
74+
font-family: var(--vscode-editor-font-family);
75+
}
76+
table p {
77+
margin-top: 0;
78+
margin-bottom: 6px;
79+
margin-left: 6px;
80+
}
81+
h3.r-arguments-title + table tr td:first-child {
82+
vertical-align: top;
83+
min-width: 24px;
84+
padding-right: 12px;
85+
}
86+
hr {
87+
height: 1.5px;
88+
border: none;
89+
background-color: var(--vscode-textBlockQuote-border);
90+
}

0 commit comments

Comments
 (0)