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

Account for Windows and MSFT Edge limitations when loading WebViews #42

Merged
merged 9 commits into from
Sep 27, 2024
4 changes: 3 additions & 1 deletion plugin/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0",
com.google.gson;bundle-version="2.11.0",
org.eclipse.ui.ide;bundle-version="3.22.200",
org.eclipse.ui.workbench.texteditor;bundle-version="3.17.400",
org.eclipse.mylyn.commons.ui;bundle-version="4.2.0"
org.eclipse.mylyn.commons.ui;bundle-version="4.2.0",
org.eclipse.jetty.server;bundle-version="12.0.9",
org.eclipse.jetty.util;bundle-version="12.0.9"
Bundle-Classpath: .,
target/dependency/animal-sniffer-annotations-1.9.jar,
target/dependency/annotations-2.25.33.jar,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package software.aws.toolkits.eclipse.amazonq.lsp.connection;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand All @@ -13,14 +15,15 @@

public class AuthLspConnectionProvider extends AbstractLspConnectionProvider {

public AuthLspConnectionProvider() throws IOException {
public AuthLspConnectionProvider() throws IOException, URISyntaxException {
super();
var authJs = PluginUtils.getResource("auth/packages/server/dist/index.js");
var authJsPath = Path.of(authJs.toURI()).toString();
var lspManager = LspManagerProvider.getInstance();

List<String> commands = new ArrayList<>();
commands.add(lspManager.getLspInstallation().nodeExecutable().toString());
commands.add(authJs.getPath());
commands.add(authJsPath);
commands.add("--nolazy");
commands.add("--inspect=5599");
commands.add("--stdio");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,19 @@ public LspInstallation getLspInstallation() {
}

private static void makeExecutable(final Path filePath) throws IOException {
if (!hasPosixFilePermissions(filePath)) {
return;
}
var permissions = new HashSet<>(Arrays.asList(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_EXECUTE));
Files.setPosixFilePermissions(filePath, permissions);
}

private static boolean hasPosixFilePermissions(final Path path) {
return path.getFileSystem().supportedFileAttributeViews().contains("posix");
}

private static Path findFileWithPrefix(final Path directory, final String prefix) throws IOException {
try (var paths = Files.walk(directory)) {
return paths
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package software.aws.toolkits.eclipse.amazonq.util;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.util.resource.ResourceFactory;

public final class WebviewAssetServer {

private Server server;

/**
* Sets up virtual host mapping for the given path using Jetty server.
* @param jsPath
* @return boolean indicating if server can be successfully launched
*/
public boolean resolve(final String jsPath) {
try {
server = new Server(0);
var servletContext = new ContextHandler();
servletContext.setContextPath("/");
servletContext.addVirtualHosts(new String[] {"localhost"});

var handler = new ResourceHandler();

ResourceFactory resourceFactory = ResourceFactory.of(server);
handler.setBaseResource(resourceFactory.newResource(jsPath));
handler.setDirAllowed(true);
servletContext.setHandler(handler);

server.setHandler(servletContext);
server.start();
return true;

} catch (Exception e) {
stop();
PluginLogger.error("Error occurred while attempting to start a virtual server for " + jsPath, e);
return false;
}
}

public String getUri() {
return server.getURI().toString();
}

public void stop() {
if (server != null) {
try {
server.stop();
} catch (Exception e) {
PluginLogger.error("Error occurred when attempting to stop the virtual server", e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package software.aws.toolkits.eclipse.amazonq.views;

import java.nio.file.Path;
import org.eclipse.swt.browser.BrowserFunction;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
Expand All @@ -11,13 +12,15 @@
import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;
import software.aws.toolkits.eclipse.amazonq.util.PluginUtils;
import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils;
import software.aws.toolkits.eclipse.amazonq.util.WebviewAssetServer;
import software.aws.toolkits.eclipse.amazonq.views.actions.AmazonQCommonActions;

public class AmazonQChatWebview extends AmazonQView {

public static final String ID = "software.aws.toolkits.eclipse.amazonq.views.AmazonQChatWebview";

private AmazonQCommonActions amazonQCommonActions;
private WebviewAssetServer webviewAssetServer;

private final ViewCommandParser commandParser;
private final ViewActionHandler actionHandler;
Expand Down Expand Up @@ -55,20 +58,35 @@ public Object function(final Object[] arguments) {

private String getContent() {
String jsFile = PluginUtils.getAwsDirectory(LspConstants.LSP_SUBDIRECTORY).resolve("amazonq-ui.js").toString();
var jsParent = Path.of(jsFile).getParent();
var jsDirectoryPath = Path.of(jsParent.toUri()).normalize().toString();

webviewAssetServer = new WebviewAssetServer();
var result = webviewAssetServer.resolve(jsDirectoryPath);
if (!result) {
return "Failed to load JS";
}

var chatJsPath = webviewAssetServer.getUri() + "amazonq-ui.js";
return String.format("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src %s 'unsafe-inline'; style-src %s 'unsafe-inline';
img-src 'self' data:; object-src 'none'; base-uri 'none'; upgrade-insecure-requests;"
>
<title>Chat UI</title>
%s
</head>
<body>
%s
</body>
</html>
""", generateCss(), generateJS(jsFile));
""", chatJsPath, chatJsPath, generateCss(), generateJS(chatJsPath));
}

private String generateCss() {
Expand Down Expand Up @@ -120,4 +138,11 @@ protected final void handleAuthStatusChange(final boolean isLoggedIn) {
});
}

@Override
public final void dispose() {
if (webviewAssetServer != null) {
webviewAssetServer.stop();
}
super.dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package software.aws.toolkits.eclipse.amazonq.views;

import java.util.Set;

import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.graphics.Color;
Expand All @@ -17,6 +16,8 @@
import software.aws.toolkits.eclipse.amazonq.util.AuthStatusChangedListener;
import software.aws.toolkits.eclipse.amazonq.util.AuthUtils;
import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;
import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform;
import software.aws.toolkits.eclipse.amazonq.util.PluginUtils;
import software.aws.toolkits.eclipse.amazonq.views.actions.AmazonQCommonActions;

public abstract class AmazonQView extends ViewPart {
Expand Down Expand Up @@ -78,14 +79,22 @@ protected final void setupAmazonQView(final Composite parent, final boolean isLo
}

private void setupBrowser(final Composite parent) {
browser = new Browser(parent, SWT.NATIVE);
browser = new Browser(parent, getBrowserStyle());
Display display = Display.getCurrent();
Color black = display.getSystemColor(SWT.COLOR_BLACK);

browser.setBackground(black);
parent.setBackground(black);
}

private int getBrowserStyle() {
var platform = PluginUtils.getPlatform();
if (platform == PluginPlatform.WINDOWS) {
return SWT.EDGE;
}
return SWT.WEBKIT;
}

private void setupActions(final Browser browser, final boolean isLoggedIn) {
amazonQCommonActions = new AmazonQCommonActions(browser, isLoggedIn, getViewSite());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@
package software.aws.toolkits.eclipse.amazonq.views;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;

import org.eclipse.swt.browser.BrowserFunction;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import software.aws.toolkits.eclipse.amazonq.util.AuthUtils;
import software.aws.toolkits.eclipse.amazonq.util.PluginUtils;
import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils;
import software.aws.toolkits.eclipse.amazonq.util.WebviewAssetServer;
import software.aws.toolkits.eclipse.amazonq.views.actions.AmazonQCommonActions;

public final class ToolkitLoginWebview extends AmazonQView {

public static final String ID = "software.aws.toolkits.eclipse.amazonq.views.ToolkitLoginWebview";

private AmazonQCommonActions amazonQCommonActions;
private WebviewAssetServer webviewAssetServer;

private final ViewCommandParser commandParser;
private final ViewActionHandler actionHandler;
Expand Down Expand Up @@ -64,6 +68,16 @@ protected void handleAuthStatusChange(final boolean isLoggedIn) {
private String getContent() {
try {
URL jsFile = PluginUtils.getResource("webview/build/assets/js/getStart.js");
var jsParent = Path.of(jsFile.toURI()).getParent();
var jsDirectoryPath = Path.of(jsParent.toUri()).normalize().toString();

webviewAssetServer = new WebviewAssetServer();
var result = webviewAssetServer.resolve(jsDirectoryPath);
if (!result) {
return "Failed to load JS";
}
var loginJsPath = webviewAssetServer.getUri() + "getStart.js";

return String.format("""
<!DOCTYPE html>
<html>
Expand All @@ -85,10 +99,17 @@ private String getContent() {
</script>
</body>
</html>
""", jsFile.toString());
} catch (IOException e) {
""", loginJsPath);
} catch (IOException | URISyntaxException e) {
return "Failed to load JS";
}
}

@Override
public void dispose() {
if (webviewAssetServer != null) {
webviewAssetServer.stop();
}
super.dispose();
}
}