Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix interactive R and Python plots on Server Web and Workbench #4855

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions extensions/positron-proxy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"onCommand:positronProxy.startHelpProxyServer",
"onCommand:positronProxy.setHelpProxyServerStyles",
"onCommand:positronProxy.startHtmlProxyServer",
"onCommand:positronProxy.startHttpProxyServer",
"onStartupFinished"
],
"main": "./out/extension.js",
Expand Down
14 changes: 11 additions & 3 deletions extensions/positron-proxy/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,19 @@ export function activate(context: vscode.ExtensionContext) {
)
);

// Register the positronProxy.stopHelpProxyServer command and add its disposable.
// Register the positronProxy.startHttpProxyServer command and add its disposable.
context.subscriptions.push(
vscode.commands.registerCommand(
'positronProxy.stopHelpProxyServer',
(targetOrigin: string) => positronProxy.stopHelpProxyServer(targetOrigin)
'positronProxy.startHttpProxyServer',
async (targetOrigin: string) => await positronProxy.startHttpProxyServer(targetOrigin)
)
);

// Register the positronProxy.stopProxyServer command and add its disposable.
context.subscriptions.push(
vscode.commands.registerCommand(
'positronProxy.stopProxyServer',
(targetOrigin: string) => positronProxy.stopProxyServer(targetOrigin)
)
);

Expand Down
90 changes: 66 additions & 24 deletions extensions/positron-proxy/src/positronProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class PositronProxy implements Disposable {
//#region Private Properties

/**
* Gets or sets a value which indicates whether the resources/scripts.html file has been loaded.
* Gets or sets a value which indicates whether the resources/scripts_{TYPE}.html files have been loaded.
*/
private _scriptsFileLoaded = false;

Expand Down Expand Up @@ -141,11 +141,13 @@ export class PositronProxy implements Disposable {
* @param context The extension context.
*/
constructor(private readonly context: ExtensionContext) {
// Try to load the resources/scripts.html file and the elements within it. This will either
// Try to load the resources/scripts_{TYPE}.html files and the elements within them. This will either
// work or it will not work, but there's not sense in trying it again, if it doesn't.

// Load the scripts_help.html file for the help proxy server.
try {
// Load the resources/scripts.html scripts file.
const scriptsPath = path.join(this.context.extensionPath, 'resources', 'scripts.html');
// Load the resources/scripts_help.html scripts file.
const scriptsPath = path.join(this.context.extensionPath, 'resources', 'scripts_help.html');
const scripts = fs.readFileSync(scriptsPath).toString('utf8');

// Get the elements from the scripts file.
Expand All @@ -159,7 +161,7 @@ export class PositronProxy implements Disposable {
this._helpStyleOverrides !== undefined &&
this._helpScript !== undefined;
} catch (error) {
console.log(`Failed to load the resources/scripts.html file.`);
console.log(`Failed to load the resources/scripts_help.html file.`);
}
}

Expand Down Expand Up @@ -225,36 +227,21 @@ export class PositronProxy implements Disposable {
</head>`
);

// When running on Web, we need to prepend root-relative URLs with the proxy path,
// because the help proxy server is running at a different origin than the target origin.
// When running on Desktop, we don't need to do this, because the help proxy server is
// running at the same origin as the target origin (localhost).
if (vscode.env.uiKind === vscode.UIKind.Web) {
// Prepend root-relative URLs with the proxy path. The proxy path may look like
// /proxy/<PORT> or a different proxy path if an external uri is used.
response = response.replace(
// This is icky and we should use a proper HTML parser, but it works for now.
// Possible sources of error are: whitespace differences, single vs. double
// quotes, etc., which are not covered in this regex.
// Regex translation: look for src="/ or href="/ and replace it with
// src="<PROXY_PATH> or href="<PROXY_PATH> respectively.
/(src|href)="\/([^"]+)"/g,
`$1="${proxyPath}/$2"`
);
}
// Rewrite the URLs with the proxy path.
response = this.rewriteUrlsWithProxyPath(response, proxyPath);

// Return the response.
return response;
});
}

/**
* Stops a help proxy server.
* Stops a proxy server.
* @param targetOrigin The target origin.
* @returns A value which indicates whether the proxy server for the target origin was found and
* stopped.
*/
stopHelpProxyServer(targetOrigin: string): boolean {
stopProxyServer(targetOrigin: string): boolean {
// See if we have a proxy server for the target origin. If we do, stop it.
const proxyServer = this._proxyServers.get(targetOrigin);
if (proxyServer) {
Expand Down Expand Up @@ -291,6 +278,32 @@ export class PositronProxy implements Disposable {
this._helpStyles = styles;
}

/**
* Starts an HTTP proxy server.
* @param targetOrigin The target origin.
* @returns The server origin.
*/
startHttpProxyServer(targetOrigin: string): Promise<string> {
// Start the proxy server.
return this.startProxyServer(
targetOrigin,
async (serverOrigin, proxyPath, url, contentType, responseBuffer) => {
// If this isn't 'text/html' content, just return the response buffer.
if (!contentType.includes('text/html')) {
return responseBuffer;
}

// Get the response.
let response = responseBuffer.toString('utf8');

// Rewrite the URLs with the proxy path.
response = this.rewriteUrlsWithProxyPath(response, proxyPath);

// Return the response.
return response;
});
}

//#endregion Public Methods

//#region Private Methods
Expand Down Expand Up @@ -369,5 +382,34 @@ export class PositronProxy implements Disposable {
});
}

/**
* Rewrites the URLs in the content.
* @param content The content.
* @param proxyPath The proxy path.
* @returns The content with the URLs rewritten.
*/
rewriteUrlsWithProxyPath(content: string, proxyPath: string): string {
// When running on Web, we need to prepend root-relative URLs with the proxy path,
// because the help proxy server is running at a different origin than the target origin.
// When running on Desktop, we don't need to do this, because the help proxy server is
// running at the same origin as the target origin (localhost).
if (vscode.env.uiKind === vscode.UIKind.Web) {
// Prepend root-relative URLs with the proxy path. The proxy path may look like
// /proxy/<PORT> or a different proxy path if an external uri is used.
return content.replace(
// This is icky and we should use a proper HTML parser, but it works for now.
// Possible sources of error are: whitespace differences, single vs. double
// quotes, etc., which are not covered in this regex.
// Regex translation: look for src="/ or href="/ and replace it with
// src="<PROXY_PATH> or href="<PROXY_PATH> respectively.
/(src|href)="\/([^"]+)"/g,
`$1="${proxyPath}/$2"`
);
}

// Return the content as-is.
return content;
}

//#endregion Private Methods
}
12 changes: 10 additions & 2 deletions src/vs/code/browser/workbench/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { parse } from 'vs/base/common/marshalling';
import { Schemas } from 'vs/base/common/network';
import { posix } from 'vs/base/common/path';
// --- Start PWB ---
import { join, posix } from 'vs/base/common/path';
// --- End PWB ---
import { isEqual } from 'vs/base/common/resources';
import { ltrim } from 'vs/base/common/strings';
import { URI, UriComponents } from 'vs/base/common/uri';
Expand Down Expand Up @@ -614,7 +616,13 @@ function readCookie(name: string): string | undefined {
.replace('/p/', '/proxy/')
.replace('{{port}}', localhostMatch.port.toString());
}
resolvedUri = URI.parse(new URL(renderedTemplate, mainWindow.location.href).toString());
// Update the authority and path of the URI to point to the proxy server. This
// retains the original query and fragment, while updating the authority and
// path to the proxy server.
resolvedUri = resolvedUri.with({
authority: mainWindow.location.host,
path: join(mainWindow.location.pathname, renderedTemplate, resolvedUri.path),
});
} else {
throw new Error(`Failed to resolve external URI: ${uri.toString()}. Could not determine base url because productConfiguration missing.`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ class PositronHelpService extends Disposable implements IPositronHelpService {
cleanupTargetOrigins.forEach(targetOrigin => {
if (!activeTargetOrigins.includes(targetOrigin)) {
this._commandService.executeCommand<boolean>(
'positronProxy.stopHelpProxyServer',
'positronProxy.stopProxyServer',
targetOrigin
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,28 @@ export class UiClientInstance extends Disposable {
}

/**
* Starts an HTML proxy server for the given HTML file.
* Starts a proxy server for the given HTML file or server url.
*
* @param htmlPath The path to the HTML file to open
* @returns A URI representing the HTML file
* @param targetPath The path to the HTML file or server url to open
* @returns A URI representing the HTML file or server url
*/
private async startHtmlProxyServer(htmlPath: string): Promise<URI> {
const url = await this._commandService.executeCommand<string>(
'positronProxy.startHtmlProxyServer',
htmlPath
);
private async startHtmlProxyServer(targetPath: string): Promise<URI> {
const uriScheme = URI.parse(targetPath).scheme;
let url;

if (uriScheme === 'file') {
// If the path is for a file, start an HTML proxy server.
url = await this._commandService.executeCommand<string>(
'positronProxy.startHtmlProxyServer',
targetPath
);
} else if (uriScheme === 'http' || uriScheme === 'https') {
// If the path is for a server, start a generic proxy server.
url = await this._commandService.executeCommand<string>(
'positronProxy.startHttpProxyServer',
targetPath
);
}

if (!url) {
throw new Error('Failed to start HTML file proxy server');
Expand Down