diff --git a/.gitpod.yml b/.gitpod.yml index 933056a4cb217c..de947ec21474c2 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -33,6 +33,7 @@ tasks: init: | leeway exec --package components/supervisor-api/java:lib --package components/gitpod-protocol/java:lib -- ./gradlew build leeway exec --package components/ide/jetbrains/backend-plugin:plugin -- ./gradlew buildPlugin + leeway exec --package components/ide/jetbrains/gateway-plugin:plugin -- ./gradlew buildPlugin - name: TypeScript before: scripts/branch-namespace.sh init: yarn --network-timeout 100000 && yarn build diff --git a/.werft/build.ts b/.werft/build.ts index 25ef01878e4bc0..420f9216281fb5 100644 --- a/.werft/build.ts +++ b/.werft/build.ts @@ -149,6 +149,7 @@ export async function build(context, version) { const storage = buildConfig["storage"] || ""; const withIntegrationTests = "with-integration-tests" in buildConfig; const publishToNpm = "publish-to-npm" in buildConfig || mainBuild; + const publishToJBMarketplace = "publish-to-jb-marketplace" in buildConfig || mainBuild; const analytics = buildConfig["analytics"]; const localAppVersion = mainBuild || ("with-localapp-version" in buildConfig) ? version : "unknown"; const retag = ("with-retag" in buildConfig) ? "" : "--dont-retag"; @@ -171,6 +172,7 @@ export async function build(context, version) { storage: storage, withIntegrationTests, publishToNpm, + publishToJBMarketplace, analytics, localAppVersion, retag, @@ -205,7 +207,7 @@ export async function build(context, version) { if (withContrib || publishRelease) { exec(`leeway build --docker-build-options network=host --werft=true -c remote ${dontTest ? '--dont-test' : ''} -Dversion=${version} -DimageRepoBase=${imageRepo} contrib:all`); } - exec(`leeway build --docker-build-options network=host --werft=true -c remote ${dontTest ? '--dont-test' : ''} ${retag} --coverage-output-path=${coverageOutput} -Dversion=${version} -DremoveSources=false -DimageRepoBase=${imageRepo} -DlocalAppVersion=${localAppVersion} -DSEGMENT_IO_TOKEN=${process.env.SEGMENT_IO_TOKEN} -DnpmPublishTrigger=${publishToNpm ? Date.now() : 'false'}`); + exec(`leeway build --docker-build-options network=host --werft=true -c remote ${dontTest ? '--dont-test' : ''} ${retag} --coverage-output-path=${coverageOutput} -Dversion=${version} -DremoveSources=false -DimageRepoBase=${imageRepo} -DlocalAppVersion=${localAppVersion} -DSEGMENT_IO_TOKEN=${process.env.SEGMENT_IO_TOKEN} -DnpmPublishTrigger=${publishToNpm ? Date.now() : 'false'} -DjbMarketplacePublishTrigger=${publishToJBMarketplace ? Date.now() : 'false'}`); if (publishRelease) { try { werft.phase("publish", "checking version semver compliance..."); diff --git a/.werft/build.yaml b/.werft/build.yaml index 14834be69f24c3..5594a8f6e6ad0b 100644 --- a/.werft/build.yaml +++ b/.werft/build.yaml @@ -123,6 +123,11 @@ pod: secretKeyRef: name: npm-auth-token key: npm-auth-token.json + - name: JB_MARKETPLACE_PUBLISH_TOKEN + valueFrom: + secretKeyRef: + name: jb-marketplace-publish-token + key: token - name: SLACK_NOTIFICATION_PATH valueFrom: secretKeyRef: diff --git a/.werft/debug.yaml b/.werft/debug.yaml index 5c9127c5cc4ed8..56cf457e2f8129 100644 --- a/.werft/debug.yaml +++ b/.werft/debug.yaml @@ -107,6 +107,11 @@ pod: secretKeyRef: name: npm-auth-token key: npm-auth-token.json + - name: JB_MARKETPLACE_PUBLISH_TOKEN + valueFrom: + secretKeyRef: + name: jb_marketplace_publish_token + key: token - name: SLACK_NOTIFICATION_PATH valueFrom: secretKeyRef: diff --git a/WORKSPACE.yaml b/WORKSPACE.yaml index a2903cf4d064f8..5e38df1b151f92 100644 --- a/WORKSPACE.yaml +++ b/WORKSPACE.yaml @@ -4,6 +4,8 @@ defaultArgs: coreYarnLockBase: ../.. npmPublishTrigger: "false" publishToNPM: true + jbMarketplacePublishTrigger: "false" + publishToJBMarketplace: true localAppVersion: unknown codeCommit: 40c9a1fb052740734bf465b98032f1a50404c042 intellijDownloadUrl: "https://download.jetbrains.com/idea/ideaIU-2021.3.1.tar.gz" diff --git a/chart/templates/server-ide-configmap.yaml b/chart/templates/server-ide-configmap.yaml index 3ec8a51dc431ee..831ee3728df866 100644 --- a/chart/templates/server-ide-configmap.yaml +++ b/chart/templates/server-ide-configmap.yaml @@ -78,28 +78,24 @@ options: title: "IntelliJ IDEA" type: "desktop" logo: "https://ide.{{ $.Values.hostname }}/image/ide-logo/intellijIdeaLogo.svg" - notes: ["While in beta, when you open a workspace with IntelliJ IDEA you will need to use the password “gitpod”."] image: {{ (include "gitpod.comp.imageFull" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.desktopIdeImages.intellij)) }} goland: orderKey: "05" title: "GoLand" type: "desktop" logo: "https://ide.{{ $.Values.hostname }}/image/ide-logo/golandLogo.svg" - notes: ["While in beta, when you open a workspace with GoLand you will need to use the password “gitpod”."] image: {{ (include "gitpod.comp.imageFull" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.desktopIdeImages.goland)) }} pycharm: orderKey: "06" title: "PyCharm" type: "desktop" logo: "https://ide.{{ $.Values.hostname }}/image/ide-logo/pycharmLogo.svg" - notes: ["While in beta, when you open a workspace with PyCharm you will need to use the password “gitpod”."] image: {{ (include "gitpod.comp.imageFull" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.desktopIdeImages.pycharm)) }} phpstorm: orderKey: "07" title: "PhpStorm" type: "desktop" logo: "https://ide.{{ $.Values.hostname }}/image/ide-logo/phpstormLogo.svg" - notes: ["While in beta, when you open a workspace with PhpStorm you will need to use the password “gitpod”."] image: {{ (include "gitpod.comp.imageFull" (dict "root" $ "gp" $gp "comp" $gp.components.workspace.desktopIdeImages.phpstorm)) }} defaultIde: "code" @@ -122,7 +118,7 @@ clients: defaultDesktopIDE: "intellij" desktopIDEs: ["intellij", "goland", "pycharm", "phpstorm"] installationSteps: [ - "If you don't see an open dialog by the browser, make sure you have JetBrains Gateway with Gitpod Plugin installed on your machine, and then click ${OPEN_LINK_LABEL} below.", + "If you don't see an open dialog by the browser, make sure you have JetBrains Gateway with Gitpod Plugin installed on your machine, and then click ${OPEN_LINK_LABEL} below.", ] {{ end }} diff --git a/components/BUILD.yaml b/components/BUILD.yaml index 192874e5b69335..a35555a321bb5c 100644 --- a/components/BUILD.yaml +++ b/components/BUILD.yaml @@ -79,6 +79,7 @@ packages: - components/local-app-api/typescript-grpcweb:publish - components/supervisor-api/typescript-grpc:publish - components/supervisor-api/typescript-grpcweb:publish + - components/ide/jetbrains/gateway-plugin:publish - name: all-apps type: generic deps: diff --git a/components/gitpod-protocol/java/build.gradle b/components/gitpod-protocol/java/build.gradle index 910c46222ed9b7..99f324696b7c73 100644 --- a/components/gitpod-protocol/java/build.gradle +++ b/components/gitpod-protocol/java/build.gradle @@ -15,7 +15,7 @@ dependencies { implementation group: 'org.eclipse.lsp4j', name: 'org.eclipse.lsp4j.jsonrpc', version: '0.12.0' implementation group: 'org.eclipse.lsp4j', name: 'org.eclipse.lsp4j.websocket', version: '0.12.0' compileOnly group: 'javax.websocket', name: 'javax.websocket-api', version: '1.1' - implementation group: 'org.glassfish.tyrus.bundles', name: 'tyrus-standalone-client', version: '1.18' + implementation("org.eclipse.jetty.websocket:javax-websocket-client-impl:9.4.44.v20210927") } application { @@ -27,6 +27,10 @@ java { withJavadocJar() } +compileJava { + sourceCompatibility = "11" + targetCompatibility = "11" +} publishing { publications { diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/BufferingWebSocketMessageWriter.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/BufferingWebSocketMessageWriter.java new file mode 100644 index 00000000000000..e3d0b481d799ef --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/BufferingWebSocketMessageWriter.java @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api; + +import org.eclipse.lsp4j.jsonrpc.JsonRpcException; +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; +import org.eclipse.lsp4j.jsonrpc.MessageIssueException; +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.lsp4j.jsonrpc.messages.Message; + +import javax.websocket.Session; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BufferingWebSocketMessageWriter implements MessageConsumer { + + private static final Logger LOG = Logger.getLogger(BufferingWebSocketMessageWriter.class.getName()); + + private Session session; + + private final MessageJsonHandler jsonHandler; + private List buffer = new ArrayList<>(); + + public BufferingWebSocketMessageWriter(MessageJsonHandler jsonHandler) { + this.jsonHandler = jsonHandler; + } + + public synchronized void setSession(Session session) { + this.session = session; + if (this.buffer.isEmpty()) { + return; + } + List buffer = this.buffer; + this.buffer = new ArrayList<>(); + for (String msg : buffer) { + this.send(msg); + } + } + + @Override + public synchronized void consume(Message message) throws MessageIssueException, JsonRpcException { + this.send(jsonHandler.serialize(message)); + } + + private void send(String msg) { + if (this.session == null || !this.session.isOpen()) { + this.buffer.add(msg); + return; + } + try { + int length = msg.length(); + if (length <= session.getMaxTextMessageBufferSize()) { + session.getBasicRemote().sendText(msg); + } else { + int currentOffset = 0; + while (currentOffset < length) { + int currentEnd = Math.min(currentOffset + session.getMaxTextMessageBufferSize(), length); + session.getBasicRemote().sendText(msg.substring(currentOffset, currentEnd), currentEnd == length); + currentOffset = currentEnd; + } + } + } catch (IOException e) { + LOG.log(Level.WARNING, "failed to send message", e); + this.buffer.add(msg); + } + } + +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/ConnectionHelper.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/ConnectionHelper.java deleted file mode 100644 index 66bd70e9bcfeee..00000000000000 --- a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/ConnectionHelper.java +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2021 Gitpod GmbH. All rights reserved. -// Licensed under the GNU Affero General Public License (AGPL). -// See License-AGPL.txt in the project root for license information. - -package io.gitpod.gitpodprotocol.api; - -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import javax.websocket.ClientEndpointConfig; -import javax.websocket.ContainerProvider; -import javax.websocket.DeploymentException; -import javax.websocket.Session; -import javax.websocket.WebSocketContainer; - -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.eclipse.lsp4j.websocket.WebSocketEndpoint; - -public class ConnectionHelper { - - private Session session; - - public GitpodClient connect(final String uri, final String origin, final String token) - throws DeploymentException, IOException { - final GitpodClientImpl gitpodClient = new GitpodClientImpl(); - - final WebSocketEndpoint webSocketEndpoint = new WebSocketEndpoint() { - @Override - protected void configure(final Launcher.Builder builder) { - builder.setLocalService(gitpodClient).setRemoteInterface(GitpodServer.class); - } - - @Override - protected void connect(final Collection localServices, final GitpodServer remoteProxy) { - localServices.forEach(s -> ((GitpodClient) s).connect(remoteProxy)); - } - }; - - final ClientEndpointConfig.Configurator configurator = new ClientEndpointConfig.Configurator() { - @Override - public void beforeRequest(final Map> headers) { - headers.put("Origin", Arrays.asList(origin)); - headers.put("Authorization", Arrays.asList("Bearer " + token)); - } - }; - final ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create() - .configurator(configurator).build(); - final WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer(); - this.session = webSocketContainer.connectToServer(webSocketEndpoint, clientEndpointConfig, URI.create(uri)); - return gitpodClient; - } - - public void close() throws IOException { - if (this.session != null && this.session.isOpen()) { - this.session.close(); - } - } -} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodClient.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodClient.java index c5b913012d7e5d..ddd8d4d20608d2 100644 --- a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodClient.java +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodClient.java @@ -4,8 +4,29 @@ package io.gitpod.gitpodprotocol.api; -public interface GitpodClient { - void connect(GitpodServer server); +import io.gitpod.gitpodprotocol.api.entities.WorkspaceInstance; +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; - GitpodServer server(); +public class GitpodClient { + + private GitpodServer server; + + public void connect(GitpodServer server) { + this.server = server; + } + + public GitpodServer getServer() { + if (this.server == null) { + throw new IllegalStateException("not connected"); + } + return this.server; + } + + public void notifyConnect() { + } + + @JsonNotification + public void onInstanceUpdate(WorkspaceInstance instance) { + + } } diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodClientImpl.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodClientImpl.java deleted file mode 100644 index 9e956d2c9ecf20..00000000000000 --- a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodClientImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2021 Gitpod GmbH. All rights reserved. -// Licensed under the GNU Affero General Public License (AGPL). -// See License-AGPL.txt in the project root for license information. - -package io.gitpod.gitpodprotocol.api; - -public class GitpodClientImpl implements GitpodClient { - - private GitpodServer server; - - @Override - public void connect(GitpodServer server) { - this.server = server; - } - - @Override - public GitpodServer server() { - if (this.server == null) { - throw new IllegalStateException("server is null"); - } - return this.server; - } -} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java index 212022190024df..0af229018d7550 100644 --- a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServer.java @@ -4,12 +4,11 @@ package io.gitpod.gitpodprotocol.api; -import java.util.concurrent.CompletableFuture; - +import io.gitpod.gitpodprotocol.api.entities.*; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; -import io.gitpod.gitpodprotocol.api.entities.SendHeartBeatOptions; -import io.gitpod.gitpodprotocol.api.entities.User; +import java.util.List; +import java.util.concurrent.CompletableFuture; public interface GitpodServer { @JsonRequest @@ -17,4 +16,19 @@ public interface GitpodServer { @JsonRequest CompletableFuture sendHeartBeat(SendHeartBeatOptions options); + + @JsonRequest + CompletableFuture> getGitpodTokenScopes(String tokenHash); + + @JsonRequest + CompletableFuture getWorkspace(String workspaceId); + + @JsonRequest + CompletableFuture getOwnerToken(String workspaceId); + + @JsonRequest + CompletableFuture> getWorkspaces(GetWorkspacesOptions options); + + @JsonRequest + CompletableFuture getIDEOptions(); } diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServerConnection.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServerConnection.java new file mode 100644 index 00000000000000..9778f15be0e309 --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServerConnection.java @@ -0,0 +1,12 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api; + +import javax.websocket.CloseReason; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Future; + +public interface GitpodServerConnection extends Future, CompletionStage { +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServerConnectionImpl.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServerConnectionImpl.java new file mode 100644 index 00000000000000..d8b5d812d3205e --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServerConnectionImpl.java @@ -0,0 +1,43 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api; + +import javax.websocket.CloseReason; +import javax.websocket.Session; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class GitpodServerConnectionImpl extends CompletableFuture implements GitpodServerConnection { + + public static final Logger LOG = Logger.getLogger(GitpodServerConnectionImpl.class.getName()); + + private final String gitpodHost; + + private Session session; + + public GitpodServerConnectionImpl(String gitpodHost) { + this.gitpodHost = gitpodHost; + } + + public void setSession(Session session) { + this.session = session; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + Session session = this.session; + this.session = null; + if (session != null) { + try { + session.close(); + } catch (IOException e) { + LOG.log(Level.WARNING, gitpodHost + ": failed to close connection:", e); + } + } + return super.cancel(mayInterruptIfRunning); + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServerLauncher.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServerLauncher.java new file mode 100644 index 00000000000000..10322a6c504f39 --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/GitpodServerLauncher.java @@ -0,0 +1,114 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api; + +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; +import org.eclipse.lsp4j.jsonrpc.MessageIssueHandler; +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.eclipse.lsp4j.jsonrpc.services.ServiceEndpoints; +import org.eclipse.lsp4j.websocket.WebSocketMessageHandler; + +import javax.websocket.*; +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +public class GitpodServerLauncher { + + private final MessageConsumer messageReader; + private final MessageJsonHandler jsonHandler; + private final MessageIssueHandler remoteEndpoint; + private final BufferingWebSocketMessageWriter messageWriter; + private final GitpodClient client; + + private GitpodServerLauncher( + MessageConsumer messageReader, + MessageJsonHandler jsonHandler, + MessageIssueHandler remoteEndpoint, + BufferingWebSocketMessageWriter messageWriter, + GitpodClient client + ) { + this.messageReader = messageReader; + this.jsonHandler = jsonHandler; + this.remoteEndpoint = remoteEndpoint; + this.messageWriter = messageWriter; + this.client = client; + } + + public GitpodServerConnection listen( + String apiUrl, + String origin, + String userAgent, + String clientVersion, + String token + ) throws DeploymentException, IOException { + String gitpodHost = URI.create(apiUrl).getHost(); + GitpodServerConnectionImpl connection = new GitpodServerConnectionImpl(gitpodHost); + connection.setSession(ContainerProvider.getWebSocketContainer().connectToServer(new Endpoint() { + @Override + public void onOpen(Session session, EndpointConfig config) { + session.addMessageHandler(new WebSocketMessageHandler(messageReader, jsonHandler, remoteEndpoint)); + messageWriter.setSession(session); + client.notifyConnect(); + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + connection.complete(closeReason); + } + + @Override + public void onError(Session session, Throwable thr) { + GitpodServerConnectionImpl.LOG.log(Level.WARNING, gitpodHost + ": connection error:", thr); + connection.completeExceptionally(thr); + } + }, ClientEndpointConfig.Builder.create().configurator(new ClientEndpointConfig.Configurator() { + @Override + public void beforeRequest(final Map> headers) { + headers.put("Origin", Arrays.asList(origin)); + headers.put("Authorization", Arrays.asList("Bearer " + token)); + headers.put("User-Agent", Arrays.asList(userAgent)); + headers.put("X-Client-Version", Arrays.asList(clientVersion)); + } + }).build(), URI.create(apiUrl))); + return connection; + } + + public static GitpodServerLauncher create(GitpodClient client) { + return new Builder().create(client); + } + + private static class Builder extends Launcher.Builder { + + public GitpodServerLauncher create(GitpodClient client) { + setLocalService(client); + setRemoteInterface(GitpodServer.class); + MessageJsonHandler jsonHandler = createJsonHandler(); + BufferingWebSocketMessageWriter messageWriter = new BufferingWebSocketMessageWriter(jsonHandler); + MessageConsumer messageConsumer = wrapMessageConsumer(messageWriter); + org.eclipse.lsp4j.jsonrpc.Endpoint localEndpoint = ServiceEndpoints.toEndpoint(localServices); + org.eclipse.lsp4j.jsonrpc.RemoteEndpoint remoteEndpoint; + if (exceptionHandler == null) + remoteEndpoint = new org.eclipse.lsp4j.jsonrpc.RemoteEndpoint(messageConsumer, localEndpoint); + else + remoteEndpoint = new org.eclipse.lsp4j.jsonrpc.RemoteEndpoint(messageConsumer, localEndpoint, exceptionHandler); + jsonHandler.setMethodProvider(remoteEndpoint); + MessageConsumer messageReader = wrapMessageConsumer(remoteEndpoint); + client.connect(createProxy(remoteEndpoint)); + return new GitpodServerLauncher( + messageReader, + jsonHandler, + remoteEndpoint, + messageWriter, + client + ); + } + } + +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/GetWorkspacesOptions.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/GetWorkspacesOptions.java new file mode 100644 index 00000000000000..b16922a37d45fb --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/GetWorkspacesOptions.java @@ -0,0 +1,16 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. +package io.gitpod.gitpodprotocol.api.entities; + +public class GetWorkspacesOptions { + private int limit; + + public int getLimit() { + return limit; + } + + public void setLimit(int limit) { + this.limit = limit; + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/IDEClient.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/IDEClient.java new file mode 100644 index 00000000000000..57d09aaa29953c --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/IDEClient.java @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +import java.util.List; + +public class IDEClient { + + private String defaultDesktopIDE; + + private List desktopIDEs; + + public String getDefaultDesktopIDE() { + return defaultDesktopIDE; + } + + public void setDefaultDesktopIDE(String defaultDesktopIDE) { + this.defaultDesktopIDE = defaultDesktopIDE; + } + + public List getDesktopIDEs() { + return desktopIDEs; + } + + public void setDesktopIDEs(List desktopIDEs) { + this.desktopIDEs = desktopIDEs; + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/IDEOption.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/IDEOption.java new file mode 100644 index 00000000000000..7510193515eedc --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/IDEOption.java @@ -0,0 +1,48 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +public class IDEOption { + + private String orderKey; + + private String title; + + private String logo; + + private boolean hidden; + + public String getOrderKey() { + return orderKey; + } + + public void setOrderKey(String orderKey) { + this.orderKey = orderKey; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getLogo() { + return logo; + } + + public void setLogo(String logo) { + this.logo = logo; + } + + public boolean isHidden() { + return hidden; + } + + public void setHidden(boolean hidden) { + this.hidden = hidden; + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/IDEOptions.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/IDEOptions.java new file mode 100644 index 00000000000000..9b7effe9473e79 --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/IDEOptions.java @@ -0,0 +1,50 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +import java.util.Map; + +public class IDEOptions { + + private Map options; + + private String defaultIde; + + private String defaultDesktopIde; + + private Map clients; + + public Map getOptions() { + return options; + } + + public void setOptions(Map options) { + this.options = options; + } + + public String getDefaultIde() { + return defaultIde; + } + + public void setDefaultIde(String defaultIde) { + this.defaultIde = defaultIde; + } + + public String getDefaultDesktopIde() { + return defaultDesktopIde; + } + + public void setDefaultDesktopIde(String defaultDesktopIde) { + this.defaultDesktopIde = defaultDesktopIde; + } + + public Map getClients() { + return clients; + } + + public void setClients(Map clients) { + this.clients = clients; + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/Workspace.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/Workspace.java new file mode 100644 index 00000000000000..edfd80dc916e19 --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/Workspace.java @@ -0,0 +1,38 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +public class Workspace { + + private String id; + + private String contextURL; + + private WorkspaceContext context; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getContextURL() { + return contextURL; + } + + public void setContextURL(String contextURL) { + this.contextURL = contextURL; + } + + public WorkspaceContext getContext() { + return context; + } + + public void setContext(WorkspaceContext context) { + this.context = context; + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceContext.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceContext.java new file mode 100644 index 00000000000000..70e1f08f60accf --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceContext.java @@ -0,0 +1,17 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +public class WorkspaceContext { + private String normalizedContextURL; + + public String getNormalizedContextURL() { + return normalizedContextURL; + } + + public void setNormalizedContextURL(String normalizedContextURL) { + this.normalizedContextURL = normalizedContextURL; + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInfo.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInfo.java new file mode 100644 index 00000000000000..81180977d17575 --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInfo.java @@ -0,0 +1,27 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +public class WorkspaceInfo { + private Workspace workspace; + + public Workspace getWorkspace() { + return workspace; + } + + public void setWorkspace(Workspace workspace) { + this.workspace = workspace; + } + + private WorkspaceInstance latestInstance; + + public WorkspaceInstance getLatestInstance() { + return latestInstance; + } + + public void setLatestInstance(WorkspaceInstance latestInstance) { + this.latestInstance = latestInstance; + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInstance.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInstance.java new file mode 100644 index 00000000000000..d00ef341e03e36 --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInstance.java @@ -0,0 +1,56 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +public class WorkspaceInstance { + private String id; + private String workspaceId; + private WorkspaceInstanceStatus status; + private String ideUrl; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getWorkspaceId() { + return workspaceId; + } + + public void setWorkspaceId(String workspaceId) { + this.workspaceId = workspaceId; + } + + public WorkspaceInstanceStatus getStatus() { + return status; + } + + public void setStatus(WorkspaceInstanceStatus status) { + this.status = status; + } + + public String getIdeUrl() { + return ideUrl; + } + + public void setIdeUrl(String ideUrl) { + this.ideUrl = ideUrl; + } + + public static boolean isUpToDate(WorkspaceInstance current, WorkspaceInstance next) { + if (current == null) { + return false; + } + if (next == null) { + return true; + } + return current.getId().equals(next.getId()) && + WorkspacePhase.valueOf(current.getStatus().getPhase()).ordinal() >= + WorkspacePhase.valueOf(next.getStatus().getPhase()).ordinal(); + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInstanceConditions.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInstanceConditions.java new file mode 100644 index 00000000000000..fddc68e0b2cd74 --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInstanceConditions.java @@ -0,0 +1,26 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +public class WorkspaceInstanceConditions { + private String failed; + private String timeout; + + public String getFailed() { + return failed; + } + + public void setFailed(String failed) { + this.failed = failed; + } + + public String getTimeout() { + return timeout; + } + + public void setTimeout(String timeout) { + this.timeout = timeout; + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInstanceStatus.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInstanceStatus.java new file mode 100644 index 00000000000000..7e48cea9e11769 --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspaceInstanceStatus.java @@ -0,0 +1,35 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +public class WorkspaceInstanceStatus { + private String phase; + private String ownerToken; + private WorkspaceInstanceConditions conditions; + + public String getPhase() { + return phase; + } + + public void setPhase(String phase) { + this.phase = phase; + } + + public String getOwnerToken() { + return ownerToken; + } + + public void setOwnerToken(String ownerToken) { + this.ownerToken = ownerToken; + } + + public WorkspaceInstanceConditions getConditions() { + return conditions; + } + + public void setConditions(WorkspaceInstanceConditions conditions) { + this.conditions = conditions; + } +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspacePhase.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspacePhase.java new file mode 100644 index 00000000000000..64b3c73c5c16da --- /dev/null +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/api/entities/WorkspacePhase.java @@ -0,0 +1,17 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.gitpodprotocol.api.entities; + +public enum WorkspacePhase { + unknown, + preparing, + pending, + creating, + initializing, + running, + interrupted, + stopping, + stopped +} diff --git a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/testclient/TestClient.java b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/testclient/TestClient.java index 889f107c653ff0..ad3d98d94c2edf 100644 --- a/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/testclient/TestClient.java +++ b/components/gitpod-protocol/java/src/main/java/io/gitpod/gitpodprotocol/testclient/TestClient.java @@ -4,9 +4,9 @@ package io.gitpod.gitpodprotocol.testclient; -import io.gitpod.gitpodprotocol.api.ConnectionHelper; import io.gitpod.gitpodprotocol.api.GitpodClient; import io.gitpod.gitpodprotocol.api.GitpodServer; +import io.gitpod.gitpodprotocol.api.GitpodServerLauncher; import io.gitpod.gitpodprotocol.api.entities.SendHeartBeatOptions; import io.gitpod.gitpodprotocol.api.entities.User; @@ -16,18 +16,14 @@ public static void main(String[] args) throws Exception { String token = "CHANGE-ME"; String origin = "https://CHANGE-ME.gitpod.io/"; - ConnectionHelper conn = new ConnectionHelper(); - try { - GitpodClient gitpodClient = conn.connect(uri, origin, token); - GitpodServer gitpodServer = gitpodClient.server(); - User user = gitpodServer.getLoggedInUser().join(); - System.out.println("logged in user:" + user); + GitpodClient client = new GitpodClient(); + GitpodServerLauncher.create(client).listen(uri, origin, token, "Test", "Test"); + GitpodServer gitpodServer = client.getServer(); + User user = gitpodServer.getLoggedInUser().join(); + System.out.println("logged in user:" + user); - Void result = gitpodServer - .sendHeartBeat(new SendHeartBeatOptions("CHANGE-ME", false)).join(); - System.out.println("send heart beat:" + result); - } finally { - conn.close(); - } + Void result = gitpodServer + .sendHeartBeat(new SendHeartBeatOptions("CHANGE-ME", false)).join(); + System.out.println("send heart beat:" + result); } } diff --git a/components/ide/jetbrains/backend-plugin/README.md b/components/ide/jetbrains/backend-plugin/README.md index 2413bd5e54cc3c..3ff94891fc9189 100644 --- a/components/ide/jetbrains/backend-plugin/README.md +++ b/components/ide/jetbrains/backend-plugin/README.md @@ -1,15 +1,17 @@ -# Jetbrains IDE Backend Plugin +# Gitpod Remote -Jetbrains IDE backend plugin to provide support for Gitpod. +Provides integrations within a Gitpod workspace. -When installed in the headless Jetbrains IDE running in a Gitpod workspace, this plugin monitors user activity of the client IntelliJ and sends heartbeats accordingly. Avoiding the workspace timing out. -**Warning**: Currently, given the challenge of mimicking user activity in a local Jetbrains IDE, there are no automated integration tests testing the functionality of this plugin. Please be particularly careful and manually test your changes. +**Warning**: Currently, given the challenge of mimicking user activity in a local Jetbrains IDE, there are no automated +integration tests testing the functionality of this plugin. Please be particularly careful and manually test your +changes. ## Usage 1. Produce the plugin by running `./gradlew buildPlugin`. -2. Unzip `build/distributions/jetbrains-backend-plugin-1.0-SNAPSHOT.zip` to the `plugins/` folder of the headless Jetbrains IDE. +2. Unzip `build/distributions/jetbrains-backend-plugin-1.0-SNAPSHOT.zip` to the `plugins/` folder of the headless + Jetbrains IDE. 3. Start the headless Jetbrains IDE. diff --git a/components/ide/jetbrains/backend-plugin/build.gradle.kts b/components/ide/jetbrains/backend-plugin/build.gradle.kts index 409d276b1f7ac7..499182c1c8b05f 100644 --- a/components/ide/jetbrains/backend-plugin/build.gradle.kts +++ b/components/ide/jetbrains/backend-plugin/build.gradle.kts @@ -39,14 +39,8 @@ dependencies { type = "jar" } } - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.5.2") - implementation("io.ktor:ktor-client-core:1.6.3") - implementation("io.ktor:ktor-client-cio:1.6.3") - implementation("io.ktor:ktor-client-jackson:1.6.3") - + compileOnly("javax.websocket:javax.websocket-api:1.1") detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.18.1") - testImplementation(kotlin("test")) } diff --git a/components/ide/jetbrains/backend-plugin/gradle.properties b/components/ide/jetbrains/backend-plugin/gradle.properties index c6623f9235cb87..4486a61e1807d8 100644 --- a/components/ide/jetbrains/backend-plugin/gradle.properties +++ b/components/ide/jetbrains/backend-plugin/gradle.properties @@ -1,32 +1,24 @@ -version=1.0-SNAPSHOT - +version=0.0.1 # IntelliJ Platform Artifacts Repositories # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html - -pluginGroup = io.gitpod.ide.jetbrains.backend -pluginName = jetbrains-backend-plugin - +pluginGroup=io.gitpod.jetbrains +pluginName=gitpod-remote # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. -pluginSinceBuild = 213 -pluginUntilBuild = 213.* - +pluginSinceBuild=213 +pluginUntilBuild=213.* # Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl # See https://jb.gg/intellij-platform-builds-list for available build versions. -pluginVerifierIdeVersions = 2021.3.1 - +pluginVerifierIdeVersions=2021.3.1 # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties -platformType = IU -platformVersion = 213-EAP-SNAPSHOT -platformDownloadSources = true - +platformType=IU +platformVersion=213-EAP-SNAPSHOT +platformDownloadSources=true # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 -platformPlugins = - +platformPlugins= # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. -kotlin.stdlib.default.dependency = false - -supervisorApiProjectPath = ../../../supervisor-api/java -gitpodProtocolProjectPath = ../../../gitpod-protocol/java +kotlin.stdlib.default.dependency=false +supervisorApiProjectPath=../../../supervisor-api/java +gitpodProtocolProjectPath=../../../gitpod-protocol/java diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/services/ControllerStatusService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/services/ControllerStatusService.kt deleted file mode 100644 index b1e4c5a486547a..00000000000000 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/services/ControllerStatusService.kt +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2021 Gitpod GmbH. All rights reserved. -// Licensed under the GNU Affero General Public License (AGPL). -// See License-AGPL.txt in the project root for license information. - -package io.gitpod.ide.jetbrains.backend.services - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.fasterxml.jackson.annotation.JsonSetter -import com.fasterxml.jackson.annotation.Nulls -import com.intellij.openapi.diagnostic.logger -import io.gitpod.ide.jetbrains.backend.utils.Retrier.retry -import io.ktor.client.HttpClient -import io.ktor.client.features.HttpTimeout -import io.ktor.client.features.json.JacksonSerializer -import io.ktor.client.features.json.JsonFeature -import io.ktor.client.request.get -import java.io.IOException - -object ControllerStatusService { - private val logger = logger() - - private const val PORT = 63342 - private val cwmToken = System.getenv("CWM_HOST_STATUS_OVER_HTTP_TOKEN") - - private val client: HttpClient by lazy { - HttpClient { - install(HttpTimeout) { - @Suppress("MagicNumber") - requestTimeoutMillis = 2000 - } - install(JsonFeature) { - serializer = JacksonSerializer() - } - } - } - - data class ControllerStatus(val connected: Boolean, val secondsSinceLastActivity: Int) - - /** - * @throws IOException - */ - suspend fun fetch(): ControllerStatus = - @Suppress("MagicNumber") - retry(3, logger) { - @Suppress("TooGenericExceptionCaught") // Unsure what exceptions Ktor might throw - val response: Response = try { - client.get("http://localhost:$PORT/codeWithMe/unattendedHostStatus?token=$cwmToken") - } catch (e: Exception) { - throw IOException("Failed to retrieve controller status.", e) - } - - if (response.projects.isEmpty()) { - return@retry ControllerStatus(false, 0) - } - - return@retry ControllerStatus( - response.projects[0].controllerConnected, - response.projects[0].secondsSinceLastControllerActivity - ) - } - - @JsonIgnoreProperties(ignoreUnknown = true) - private data class Response( - val appPid: Int, - @JsonSetter(nulls = Nulls.AS_EMPTY) - val projects: List - ) { - @JsonIgnoreProperties(ignoreUnknown = true) - data class Project( - val controllerConnected: Boolean, - val secondsSinceLastControllerActivity: Int - ) - } -} diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/services/HeartbeatService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/services/HeartbeatService.kt deleted file mode 100644 index 5a0aa85307775b..00000000000000 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/services/HeartbeatService.kt +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2021 Gitpod GmbH. All rights reserved. -// Licensed under the GNU Affero General Public License (AGPL). -// See License-AGPL.txt in the project root for license information. - -package io.gitpod.ide.jetbrains.backend.services - -import com.intellij.openapi.Disposable -import com.intellij.openapi.components.Service -import com.intellij.openapi.diagnostic.logger -import io.gitpod.gitpodprotocol.api.ConnectionHelper -import io.gitpod.gitpodprotocol.api.entities.SendHeartBeatOptions -import io.gitpod.ide.jetbrains.backend.services.ControllerStatusService.ControllerStatus -import io.gitpod.ide.jetbrains.backend.utils.Retrier.retry -import kotlinx.coroutines.delay -import kotlinx.coroutines.future.await -import kotlinx.coroutines.runBlocking -import java.io.IOException -import java.util.concurrent.CompletableFuture -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import kotlin.concurrent.thread -import kotlin.random.Random.Default.nextInt - -@Service -class HeartbeatService : Disposable { - private val logger = logger() - - @Suppress("MagicNumber") - private val intervalInSeconds = 30 - - private val heartbeatClient = AtomicReference() - private val status = AtomicReference( - ControllerStatus( - connected = false, - secondsSinceLastActivity = 0 - ) - ) - private val closed = AtomicBoolean(false) - - init { - logger.info("Service initiating") - - @Suppress("MagicNumber") - thread(name = "gitpod-heartbeat", contextClassLoader = this.javaClass.classLoader) { - runBlocking { - while (!closed.get()) { - checkActivity(intervalInSeconds + nextInt(5, 15)) - delay(intervalInSeconds * 1000L) - } - } - } - } - - private suspend fun checkActivity(maxIntervalInSeconds: Int) { - logger.info("Checking activity") - val status = try { - ControllerStatusService.fetch() - } catch (e: IOException) { - logger.error(e.message, e) - return@checkActivity - } - val previousStatus = this.status.getAndSet(status) - - val wasClosed: Boolean? = when { - status.connected != previousStatus.connected -> !status.connected - status.connected && status.secondsSinceLastActivity <= maxIntervalInSeconds -> false - else -> null - } - - if (wasClosed != null) { - @Suppress("TooGenericExceptionCaught") - return try { - sendHeartbeat(wasClosed) - } catch (e: Exception) { - logger.error("Failed to send heartbeat with wasClosed=$wasClosed", e) - } - } - } - - /** - * @throws DeploymentException - * @throws IOException - * @throw IllegalStateException - */ - @Synchronized - private suspend fun sendHeartbeat(wasClosed: Boolean = false) { - retry(2, logger) { - if (heartbeatClient.get() == null) { - heartbeatClient.set(createHeartbeatClient()) - } - - @Suppress("TooGenericExceptionCaught") // Unsure what exceptions might be thrown - try { - heartbeatClient.get()!!(wasClosed).await() - logger.info("Heartbeat sent with wasClosed=$wasClosed") - } catch (e: Exception) { - // If connection fails for some reason, - // remove the reference to the existing server. - heartbeatClient.set(null) - throw e - } - } - } - - /** - * @throws DeploymentException - * @throws IOException - * @throws IllegalStateException - */ - private suspend fun createHeartbeatClient(): HeartbeatClient { - logger.info("Creating HeartbeatClient") - val supervisorInfo = SupervisorInfoService.fetch() - - val server = ConnectionHelper().connect( - "wss://${supervisorInfo.host.split("//").last()}/api/v1", - supervisorInfo.workspaceUrl, - supervisorInfo.authToken - ).server() - - return { wasClosed: Boolean -> - server.sendHeartBeat(SendHeartBeatOptions(supervisorInfo.instanceId, wasClosed)) - } - } - - override fun dispose() = closed.set(true) -} - -typealias HeartbeatClient = (Boolean) -> CompletableFuture diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/listeners/MyApplicationActivationListener.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/listeners/MyApplicationActivationListener.kt similarity index 83% rename from components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/listeners/MyApplicationActivationListener.kt rename to components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/listeners/MyApplicationActivationListener.kt index 6c65457e97f669..66b6b8e6f5afc2 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/listeners/MyApplicationActivationListener.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/listeners/MyApplicationActivationListener.kt @@ -2,12 +2,12 @@ // Licensed under the GNU Affero General Public License (AGPL). // See License-AGPL.txt in the project root for license information. -package io.gitpod.ide.jetbrains.backend.listeners +package io.gitpod.jetbrains.remote.listeners import com.intellij.openapi.application.ApplicationActivationListener import com.intellij.openapi.components.service import com.intellij.openapi.wm.IdeFrame -import io.gitpod.ide.jetbrains.backend.services.HeartbeatService +import io.gitpod.jetbrains.remote.services.HeartbeatService class MyApplicationActivationListener : ApplicationActivationListener { override fun applicationActivated(ideFrame: IdeFrame) { diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/ControllerStatusService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/ControllerStatusService.kt new file mode 100644 index 00000000000000..65cd6578dc9797 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/ControllerStatusService.kt @@ -0,0 +1,69 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote.services + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.gitpod.jetbrains.remote.utils.Retrier.retry +import org.jetbrains.ide.BuiltInServerManager +import java.io.IOException +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.time.Duration + +object ControllerStatusService { + private val port = BuiltInServerManager.getInstance().port + private val cwmToken = System.getenv("CWM_HOST_STATUS_OVER_HTTP_TOKEN") + private val httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS) + .connectTimeout(Duration.ofSeconds(2)) + .build() + private val jacksonMapper = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + data class ControllerStatus(val connected: Boolean, val secondsSinceLastActivity: Int) + + /** + * @throws IOException + */ + suspend fun fetch(): ControllerStatus = + @Suppress("MagicNumber") + retry(3) { + val httpRequest = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:$port/codeWithMe/unattendedHostStatus?token=$cwmToken")) + .header("Content-Type", "application/json") + .GET() + .build() + val response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()) + if (response.statusCode() !== 200) { + throw IOException("gitpod: failed to retrieve controller status: ${response.statusCode()}") + } + val status = with(jacksonMapper) { + propertyNamingStrategy = PropertyNamingStrategies.LowerCamelCaseStrategy() + readValue(response.body(), ControllerStatusResponse::class.java) + } + + if (status.projects.isNullOrEmpty()) { + return@retry ControllerStatus(false, 0) + } + + return@retry ControllerStatus( + status.projects[0].controllerConnected, + status.projects[0].secondsSinceLastControllerActivity + ) + } + + data class Project( + val controllerConnected: Boolean, + val secondsSinceLastControllerActivity: Int + ) + + private data class ControllerStatusResponse( + val projects: List? + ) + +} diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/HeartbeatService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/HeartbeatService.kt new file mode 100644 index 00000000000000..4bd59f294aa833 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/HeartbeatService.kt @@ -0,0 +1,119 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote.services + +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.extensions.PluginId +import io.gitpod.gitpodprotocol.api.GitpodClient +import io.gitpod.gitpodprotocol.api.GitpodServerLauncher +import io.gitpod.gitpodprotocol.api.entities.SendHeartBeatOptions +import io.gitpod.jetbrains.remote.services.ControllerStatusService.ControllerStatus +import kotlinx.coroutines.* +import kotlinx.coroutines.future.await +import javax.websocket.DeploymentException +import kotlin.coroutines.coroutineContext +import kotlin.random.Random.Default.nextInt + +@Service +class HeartbeatService : Disposable { + + private val job = GlobalScope.launch { + val info = SupervisorInfoService.fetch() + val client = GitpodClient() + val launcher = GitpodServerLauncher.create(client) + launch { + connectToServer(info, launcher) + } + val intervalInSeconds = 30 + var current = ControllerStatus( + connected = false, + secondsSinceLastActivity = 0 + ) + while (isActive) { + try { + val previous = current; + current = ControllerStatusService.fetch() + + val maxIntervalInSeconds = intervalInSeconds + nextInt(5, 15) + val wasClosed: Boolean? = when { + current.connected != previous.connected -> !current.connected + current.connected && current.secondsSinceLastActivity <= maxIntervalInSeconds -> false + else -> null + } + + if (wasClosed != null) { + client.server.sendHeartBeat(SendHeartBeatOptions(info.infoResponse.instanceId, wasClosed)).await() + } + } catch (t: Throwable) { + thisLogger().error("gitpod: failed to check activity:", t) + } + delay(intervalInSeconds * 1000L) + } + } + + private suspend fun connectToServer(info: SupervisorInfoService.Result, launcher: GitpodServerLauncher) { + val plugin = PluginManagerCore.getPlugin(PluginId.getId("io.gitpod.jetbrains.remote"))!! + val connect = { + val originalClassLoader = Thread.currentThread().contextClassLoader + try { + // see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003146180/comments/360000376240 + Thread.currentThread().contextClassLoader = HeartbeatService::class.java.classLoader + launcher.listen( + info.infoResponse.gitpodApi.endpoint, + info.infoResponse.gitpodHost, + plugin.pluginId.idString, + plugin.version, + info.tokenResponse.token + ) + } finally { + Thread.currentThread().contextClassLoader = originalClassLoader; + } + } + + val minReconnectionDelay = 2 * 1000L + val maxReconnectionDelay = 30 * 1000L + val reconnectionDelayGrowFactor = 1.5; + var reconnectionDelay = minReconnectionDelay; + val gitpodHost = info.infoResponse.gitpodApi.host + var closeReason: Any = "cancelled" + try { + while (coroutineContext.isActive) { + try { + val connection = connect() + thisLogger().info("$gitpodHost: connected") + reconnectionDelay = minReconnectionDelay + closeReason = connection.await() + thisLogger().warn("$gitpodHost: connection closed, reconnecting after $reconnectionDelay milliseconds: $closeReason") + } catch (t: Throwable) { + if (t is DeploymentException) { + // connection is alright, but server does not want to handshake, there is no point to try with the same token again + throw t + } + closeReason = t + thisLogger().warn( + "$gitpodHost: failed to connect, trying again after $reconnectionDelay milliseconds:", + closeReason + ) + } + delay(reconnectionDelay) + closeReason = "cancelled" + reconnectionDelay = (reconnectionDelay * reconnectionDelayGrowFactor).toLong() + if (reconnectionDelay > maxReconnectionDelay) { + reconnectionDelay = maxReconnectionDelay + } + } + } catch (t: Throwable) { + if (t !is CancellationException) { + closeReason = t + } + } + thisLogger().warn("$gitpodHost: connection permanently closed: $closeReason") + } + + override fun dispose() = job.cancel() +} diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/services/SupervisorInfoService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/SupervisorInfoService.kt similarity index 50% rename from components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/services/SupervisorInfoService.kt rename to components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/SupervisorInfoService.kt index 414b304354949f..65c466055cea57 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/services/SupervisorInfoService.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/services/SupervisorInfoService.kt @@ -2,60 +2,50 @@ // Licensed under the GNU Affero General Public License (AGPL). // See License-AGPL.txt in the project root for license information. -package io.gitpod.ide.jetbrains.backend.services +package io.gitpod.jetbrains.remote.services -import com.intellij.openapi.diagnostic.logger -import io.gitpod.ide.jetbrains.backend.utils.Retrier.retry -import io.gitpod.supervisor.api.Info +import io.gitpod.jetbrains.remote.utils.Retrier.retry import io.gitpod.supervisor.api.Info.WorkspaceInfoRequest import io.gitpod.supervisor.api.InfoServiceGrpc +import io.gitpod.supervisor.api.Token import io.gitpod.supervisor.api.Token.GetTokenRequest import io.gitpod.supervisor.api.TokenServiceGrpc import io.grpc.ManagedChannelBuilder import kotlinx.coroutines.guava.asDeferred object SupervisorInfoService { - private val logger = logger() private const val SUPERVISOR_ADDRESS = "localhost:22999" - data class Info( - val host: String, - val workspaceUrl: String, - val instanceId: String, - val authToken: String + // there should be only one channel per an application to avoid memory leak + private val channel = ManagedChannelBuilder.forTarget(SUPERVISOR_ADDRESS).usePlaintext().build() + + data class Result( + val infoResponse: io.gitpod.supervisor.api.Info.WorkspaceInfoResponse, + val tokenResponse: Token.GetTokenResponse ) @Suppress("MagicNumber") - suspend fun fetch(): Info = - retry(3, logger) { - val channel = ManagedChannelBuilder - .forTarget(SUPERVISOR_ADDRESS) - .usePlaintext() - .build() - - val infoResponse: io.gitpod.supervisor.api.Info.WorkspaceInfoResponse = InfoServiceGrpc + suspend fun fetch(): Result = + retry(3) { + // TODO(ak) retry forever only on network issues, otherwise propagate error + val infoResponse = InfoServiceGrpc .newFutureStub(channel) .workspaceInfo(WorkspaceInfoRequest.newBuilder().build()) .asDeferred() .await() val request = GetTokenRequest.newBuilder() - .setHost(infoResponse.gitpodHost.split("//").last()) + .setHost(infoResponse.gitpodApi.host) .addScope("function:sendHeartBeat") .setKind("gitpod") .build() - val response = TokenServiceGrpc + val tokenResponse = TokenServiceGrpc .newFutureStub(channel) .getToken(request) .asDeferred() .await() - Info( - host = infoResponse.gitpodHost, - workspaceUrl = infoResponse.workspaceUrl, - instanceId = infoResponse.instanceId, - authToken = response.token - ) + Result(infoResponse, tokenResponse) } } diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/utils/Retrier.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/Retrier.kt similarity index 71% rename from components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/utils/Retrier.kt rename to components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/Retrier.kt index 30bbfddb2eb1ae..6e2cb695225b06 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/ide/jetbrains/backend/utils/Retrier.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/Retrier.kt @@ -2,13 +2,13 @@ // Licensed under the GNU Affero General Public License (AGPL). // See License-AGPL.txt in the project root for license information. -package io.gitpod.ide.jetbrains.backend.utils +package io.gitpod.jetbrains.remote.utils -import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.diagnostic.thisLogger object Retrier { @Suppress("TooGenericExceptionCaught") - suspend fun retry(n: Int, logger: Logger? = null, fn: suspend () -> T): T { + suspend fun retry(n: Int, fn: suspend () -> T): T { require(n >= 0) var i = 0 while (true) { @@ -16,7 +16,7 @@ object Retrier { return fn() } catch (e: Exception) { if (i++ < n) { - logger?.error(e) + thisLogger().error(e) } else { throw e } diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/kotlinx/coroutines/guava/ListenableFuture.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/kotlinx/coroutines/guava/ListenableFuture.kt new file mode 100644 index 00000000000000..0b200890776969 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/kotlinx/coroutines/guava/ListenableFuture.kt @@ -0,0 +1,515 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.guava + +import com.google.common.util.concurrent.* +import com.google.common.util.concurrent.internal.InternalFutureFailureAccess +import com.google.common.util.concurrent.internal.InternalFutures +import kotlinx.coroutines.* +import java.util.concurrent.* +import java.util.concurrent.CancellationException +import kotlin.coroutines.* + +/** + * Starts [block] in a new coroutine and returns a [ListenableFuture] pointing to its result. + * + * The coroutine is started immediately. Passing [CoroutineStart.LAZY] to [start] throws + * [IllegalArgumentException], because Futures don't have a way to start lazily. + * + * When the created coroutine [isCompleted][Job.isCompleted], it will try to + * *synchronously* complete the returned Future with the same outcome. This will + * succeed, barring a race with external cancellation of returned [ListenableFuture]. + * + * Cancellation is propagated bidirectionally. + * + * `CoroutineContext` is inherited from this [CoroutineScope]. Additional context elements can be + * added/overlaid by passing [context]. + * + * If the context does not have a [CoroutineDispatcher], nor any other [ContinuationInterceptor] + * member, [Dispatchers.Default] is used. + * + * The parent job is inherited from this [CoroutineScope], and can be overridden by passing + * a [Job] in [context]. + * + * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging + * facilities. + * + * Note that the error and cancellation semantics of [future] are _different_ than [async]'s. + * In contrast to [Deferred], [Future] doesn't have an intermediate `Cancelling` state. If + * the returned `Future` is successfully cancelled, and `block` throws afterward, the thrown + * error is dropped, and getting the `Future`'s value will throw a `CancellationException` with + * no cause. This is to match the specification and behavior of + * `java.util.concurrent.FutureTask`. + * + * @param context added overlaying [CoroutineScope.coroutineContext] to form the new context. + * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. + * @param block the code to execute. + */ +@InternalCoroutinesApi +public fun CoroutineScope.future( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> T +): ListenableFuture { + require(!start.isLazy) { "$start start is not supported" } + val newContext = newCoroutineContext(context) + val coroutine = ListenableFutureCoroutine(newContext) + coroutine.start(start, coroutine, block) + return coroutine.future +} + +/** + * Returns a [Deferred] that is completed or failed by `this` [ListenableFuture]. + * + * Completion is non-atomic between the two promises. + * + * Cancellation is propagated bidirectionally. + * + * When `this` `ListenableFuture` completes (either successfully or exceptionally) it will try to + * complete the returned `Deferred` with the same value or exception. This will succeed, barring a + * race with cancellation of the `Deferred`. + * + * When `this` `ListenableFuture` is [successfully cancelled][java.util.concurrent.Future.cancel], + * it will cancel the returned `Deferred`. + * + * When the returned `Deferred` is [cancelled][Deferred.cancel], it will try to propagate the + * cancellation to `this` `ListenableFuture`. Propagation will succeed, barring a race with the + * `ListenableFuture` completing normally. This is the only case in which the returned `Deferred` + * will complete with a different outcome than `this` `ListenableFuture`. + */ +@OptIn(InternalCoroutinesApi::class) +public fun ListenableFuture.asDeferred(): Deferred { + /* This method creates very specific behaviour as it entangles the `Deferred` and + * `ListenableFuture`. This behaviour is the best discovered compromise between the possible + * states and interface contracts of a `Future` and the states of a `Deferred`. The specific + * behaviour is described here. + * + * When `this` `ListenableFuture` is successfully cancelled - meaning + * `ListenableFuture.cancel()` returned `true` - it will synchronously cancel the returned + * `Deferred`. This can only race with cancellation of the returned `Deferred`, so the + * `Deferred` will always be put into its "cancelling" state and (barring uncooperative + * cancellation) _eventually_ reach its "cancelled" state when either promise is successfully + * cancelled. + * + * When the returned `Deferred` is cancelled, `ListenableFuture.cancel()` will be synchronously + * called on `this` `ListenableFuture`. This will attempt to cancel the `Future`, though + * cancellation may not succeed and the `ListenableFuture` may complete in a non-cancelled + * terminal state. + * + * The returned `Deferred` may receive and suppress the `true` return value from + * `ListenableFuture.cancel()` when the task is cancelled via the `Deferred` reference to it. + * This is unavoidable, so make sure no idempotent cancellation work is performed by a + * reference-holder of the `ListenableFuture` task. The idempotent work won't get done if + * cancellation was from the `Deferred` representation of the task. + * + * This is inherently a race. See `Future.cancel()` for a description of `Future` cancellation + * semantics. See `Job` for a description of coroutine cancellation semantics. + */ + // First, try the fast-fast error path for Guava ListenableFutures. This will save allocating an + // Exception by using the same instance the Future created. + if (this is InternalFutureFailureAccess) { + val t: Throwable? = InternalFutures.tryInternalFastPathGetFailure(this) + if (t != null) { + return CompletableDeferred().also { + it.completeExceptionally(t) + } + } + } + + // Second, try the fast path for a completed Future. The Future is known to be done, so get() + // will not block, and thus it won't be interrupted. Calling getUninterruptibly() instead of + // getDone() in this known-non-interruptible case saves the volatile read that getDone() uses to + // handle interruption. + if (isDone) { + return try { + CompletableDeferred(Uninterruptibles.getUninterruptibly(this)) + } catch (e: CancellationException) { + CompletableDeferred().also { it.cancel(e) } + } catch (e: ExecutionException) { + // ExecutionException is the only kind of exception that can be thrown from a gotten + // Future. Anything else showing up here indicates a very fundamental bug in a + // Future implementation. + CompletableDeferred().also { it.completeExceptionally(e.nonNullCause()) } + } + } + + // Finally, if this isn't done yet, attach a Listener that will complete the Deferred. + val deferred = CompletableDeferred() + Futures.addCallback(this, object : FutureCallback { + override fun onSuccess(result: T?) { + // Here we work with flexible types, so we unchecked cast to trick the type system + @Suppress("UNCHECKED_CAST") + runCatching { deferred.complete(result as T) } + .onFailure { handleCoroutineException(EmptyCoroutineContext, it) } + } + + override fun onFailure(t: Throwable) { + runCatching { deferred.completeExceptionally(t) } + .onFailure { handleCoroutineException(EmptyCoroutineContext, it) } + } + }, MoreExecutors.directExecutor()) + + // ... And cancel the Future when the deferred completes. Since the return type of this method + // is Deferred, the only interaction point from the caller is to cancel the Deferred. If this + // completion handler runs before the Future is completed, the Deferred must have been + // cancelled and should propagate its cancellation. If it runs after the Future is completed, + // this is a no-op. + deferred.invokeOnCompletion { + cancel(false) + } + // Return hides the CompletableDeferred. This should prevent casting. + return object : Deferred by deferred {} +} + +/** + * Returns the cause from an [ExecutionException] thrown by a [Future.get] or similar. + * + * [ExecutionException] _always_ wraps a non-null cause when Future.get() throws. A Future cannot + * fail without a non-null `cause`, because the only way a Future _can_ fail is an uncaught + * [Exception]. + * + * If this !! throws [NullPointerException], a Future is breaking its interface contract and losing + * state - a serious fundamental bug. + */ +private fun ExecutionException.nonNullCause(): Throwable { + return this.cause!! +} + +/** + * Returns a [ListenableFuture] that is completed or failed by `this` [Deferred]. + * + * Completion is non-atomic between the two promises. + * + * When either promise successfully completes, it will attempt to synchronously complete its + * counterpart with the same value. This will succeed barring a race with cancellation. + * + * When either promise completes with an Exception, it will attempt to synchronously complete its + * counterpart with the same Exception. This will succeed barring a race with cancellation. + * + * Cancellation is propagated bidirectionally. + * + * When the returned [Future] is successfully cancelled - meaning [Future.cancel] returned true - + * [Deferred.cancel] will be synchronously called on `this` [Deferred]. This will attempt to cancel + * the `Deferred`, though cancellation may not succeed and the `Deferred` may complete in a + * non-cancelled terminal state. + * + * When `this` `Deferred` reaches its "cancelled" state with a successful cancellation - meaning it + * completes with [kotlinx.coroutines.CancellationException] - `this` `Deferred` will synchronously + * cancel the returned `Future`. This can only race with cancellation of the returned `Future`, so + * the returned `Future` will always _eventually_ reach its cancelled state when either promise is + * successfully cancelled, for their different meanings of "successfully cancelled". + * + * This is inherently a race. See [Future.cancel] for a description of `Future` cancellation + * semantics. See [Job] for a description of coroutine cancellation semantics. See + * [JobListenableFuture.cancel] for greater detail on the overlapped cancellation semantics and + * corner cases of this method. + */ +public fun Deferred.asListenableFuture(): ListenableFuture { + val listenableFuture = JobListenableFuture(this) + // This invokeOnCompletion completes the JobListenableFuture with the same result as `this` Deferred. + // The JobListenableFuture may have completed earlier if it got cancelled! See JobListenableFuture.cancel(). + invokeOnCompletion { throwable -> + if (throwable == null) { + listenableFuture.complete(getCompleted()) + } else { + listenableFuture.completeExceptionallyOrCancel(throwable) + } + } + return listenableFuture +} + +/** + * Awaits completion of `this` [ListenableFuture] without blocking a thread. + * + * This suspend function is cancellable. + * + * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function + * stops waiting for the future and immediately resumes with [CancellationException][kotlinx.coroutines.CancellationException]. + * + * This method is intended to be used with one-shot Futures, so on coroutine cancellation, the Future is cancelled as well. + * If cancelling the given future is undesired, use [Futures.nonCancellationPropagating] or + * [kotlinx.coroutines.NonCancellable]. + */ +public suspend fun ListenableFuture.await(): T { + try { + if (isDone) return Uninterruptibles.getUninterruptibly(this) + } catch (e: ExecutionException) { + // ExecutionException is the only kind of exception that can be thrown from a gotten + // Future, other than CancellationException. Cancellation is propagated upward so that + // the coroutine running this suspend function may process it. + // Any other Exception showing up here indicates a very fundamental bug in a + // Future implementation. + throw e.nonNullCause() + } + + return suspendCancellableCoroutine { cont: CancellableContinuation -> + addListener( + ToContinuation(this, cont), + MoreExecutors.directExecutor() + ) + cont.invokeOnCancellation { + cancel(false) + } + } +} + +/** + * Propagates the outcome of [futureToObserve] to [continuation] on completion. + * + * Cancellation is propagated as cancelling the continuation. If [futureToObserve] completes + * and fails, the cause of the Future will be propagated without a wrapping + * [ExecutionException] when thrown. + */ +private class ToContinuation( + val futureToObserve: ListenableFuture, + val continuation: CancellableContinuation +) : Runnable { + override fun run() { + if (futureToObserve.isCancelled) { + continuation.cancel() + } else { + try { + continuation.resume(Uninterruptibles.getUninterruptibly(futureToObserve)) + } catch (e: ExecutionException) { + // ExecutionException is the only kind of exception that can be thrown from a gotten + // Future. Anything else showing up here indicates a very fundamental bug in a + // Future implementation. + continuation.resumeWithException(e.nonNullCause()) + } + } + } +} + +/** + * An [AbstractCoroutine] intended for use directly creating a [ListenableFuture] handle to + * completion. + * + * If [future] is successfully cancelled, cancellation is propagated to `this` `Coroutine`. + * By documented contract, a [Future] has been cancelled if + * and only if its `isCancelled()` method returns true. + * + * Any error that occurs after successfully cancelling a [ListenableFuture] is lost. + * The contract of [Future] does not permit it to return an error after it is successfully cancelled. + * On the other hand, we can't report an unhandled exception to [CoroutineExceptionHandler], + * otherwise [Future.cancel] can lead to an app crash which arguably is a contract violation. + * In contrast to [Future] which can't change its outcome after a successful cancellation, + * cancelling a [Deferred] places that [Deferred] in the cancelling/cancelled states defined by [Job], + * which _can_ show the error. + * + * This may be counterintuitive, but it maintains the error and cancellation contracts of both + * the [Deferred] and [ListenableFuture] types, while permitting both kinds of promise to point + * to the same running task. + */ +@InternalCoroutinesApi +private class ListenableFutureCoroutine( + context: CoroutineContext +) : AbstractCoroutine(context, initParentJob = true, active = true) { + + // JobListenableFuture propagates external cancellation to `this` coroutine. See JobListenableFuture. + @JvmField + val future = JobListenableFuture(this) + + override fun onCompleted(value: T) { + future.complete(value) + } + + override fun onCancelled(cause: Throwable, handled: Boolean) { + // Note: if future was cancelled in a race with a cancellation of this + // coroutine, and the future was successfully cancelled first, the cause of coroutine + // cancellation is dropped in this promise. A Future can only be completed once. + // + // This is consistent with FutureTask behaviour. A race between a Future.cancel() and + // a FutureTask.setException() for the same Future will similarly drop the + // cause of a failure-after-cancellation. + future.completeExceptionallyOrCancel(cause) + } +} + +/** + * A [ListenableFuture] that delegates to an internal [SettableFuture], collaborating with it. + * + * This setup allows the returned [ListenableFuture] to maintain the following properties: + * + * - Correct implementation of [Future]'s happens-after semantics documented for [get], [isDone] + * and [isCancelled] methods + * - Cancellation propagation both to and from [Deferred] + * - Correct cancellation and completion semantics even when this [ListenableFuture] is combined + * with different concrete implementations of [ListenableFuture] + * - Fully correct cancellation and listener happens-after obeying [Future] and + * [ListenableFuture]'s documented and implicit contracts is surprisingly difficult to achieve. + * The best way to be correct, especially given the fun corner cases from + * [AbstractFuture.setFuture], is to just use an [AbstractFuture]. + * - To maintain sanity, this class implements [ListenableFuture] and uses an auxiliary [SettableFuture] + * around coroutine's result as a state engine to establish happens-after-completion. This + * could probably be compressed into one subclass of [AbstractFuture] to save an allocation, at the + * cost of the implementation's readability. + */ +private class JobListenableFuture(private val jobToCancel: Job) : ListenableFuture { + /** + * Serves as a state machine for [Future] cancellation. + * + * [AbstractFuture] has a highly-correct atomic implementation of `Future`'s completion and + * cancellation semantics. By using that type, the [JobListenableFuture] can delegate its semantics to + * `auxFuture.get()` the result in such a way that the `Deferred` is always complete when returned. + * + * To preserve Coroutine's [CancellationException], this future points to either `T` or [Cancelled]. + */ + private val auxFuture = SettableFuture.create() + + /** + * `true` if [auxFuture.get][ListenableFuture.get] throws [ExecutionException]. + * + * Note: this is eventually consistent with the state of [auxFuture]. + * + * Unfortunately, there's no API to figure out if [ListenableFuture] throws [ExecutionException] + * apart from calling [ListenableFuture.get] on it. To avoid unnecessary [ExecutionException] allocation + * we use this field as an optimization. + */ + private var auxFutureIsFailed: Boolean = false + + /** + * When the attached coroutine [isCompleted][Job.isCompleted] successfully + * its outcome should be passed to this method. + * + * This should succeed barring a race with external cancellation. + */ + fun complete(result: T): Boolean = auxFuture.set(result) + + /** + * When the attached coroutine [isCompleted][Job.isCompleted] [exceptionally][Job.isCancelled] + * its outcome should be passed to this method. + * + * This method will map coroutine's exception into corresponding Future's exception. + * + * This should succeed barring a race with external cancellation. + */ + // CancellationException is wrapped into `Cancelled` to preserve original cause and message. + // All the other exceptions are delegated to SettableFuture.setException. + fun completeExceptionallyOrCancel(t: Throwable): Boolean = + if (t is CancellationException) auxFuture.set(Cancelled(t)) + else auxFuture.setException(t).also { if (it) auxFutureIsFailed = true } + + /** + * Returns cancellation _in the sense of [Future]_. This is _not_ equivalent to + * [Job.isCancelled]. + * + * When done, this Future is cancelled if its [auxFuture] is cancelled, or if [auxFuture] + * contains [CancellationException]. + * + * See [cancel]. + */ + override fun isCancelled(): Boolean { + // This expression ensures that isCancelled() will *never* return true when isDone() returns false. + // In the case that the deferred has completed with cancellation, completing `this`, its + // reaching the "cancelled" state with a cause of CancellationException is treated as the + // same thing as auxFuture getting cancelled. If the Job is in the "cancelling" state and + // this Future hasn't itself been successfully cancelled, the Future will return + // isCancelled() == false. This is the only discovered way to reconcile the two different + // cancellation contracts. + return auxFuture.isCancelled || isDone && !auxFutureIsFailed && try { + Uninterruptibles.getUninterruptibly(auxFuture) is Cancelled + } catch (e: CancellationException) { + // `auxFuture` got cancelled right after `auxFuture.isCancelled` returned false. + true + } catch (e: ExecutionException) { + // `auxFutureIsFailed` hasn't been updated yet. + auxFutureIsFailed = true + false + } + } + + /** + * Waits for [auxFuture] to complete by blocking, then uses its `result` + * to get the `T` value `this` [ListenableFuture] is pointing to or throw a [CancellationException]. + * This establishes happens-after ordering for completion of the entangled coroutine. + * + * [SettableFuture.get] can only throw [CancellationException] if it was cancelled externally. + * Otherwise it returns [Cancelled] that encapsulates outcome of the entangled coroutine. + * + * [auxFuture] _must be complete_ in order for the [isDone] and [isCancelled] happens-after + * contract of [Future] to be correctly followed. + */ + override fun get(): T { + return getInternal(auxFuture.get()) + } + + /** See [get()]. */ + override fun get(timeout: Long, unit: TimeUnit): T { + return getInternal(auxFuture.get(timeout, unit)) + } + + /** See [get()]. */ + private fun getInternal(result: Any): T = if (result is Cancelled) { + throw CancellationException().initCause(result.exception) + } else { + // We know that `auxFuture` can contain either `T` or `Cancelled`. + @Suppress("UNCHECKED_CAST") + result as T + } + + override fun addListener(listener: Runnable, executor: Executor) { + auxFuture.addListener(listener, executor) + } + + override fun isDone(): Boolean { + return auxFuture.isDone + } + + /** + * Tries to cancel [jobToCancel] if `this` future was cancelled. This is fundamentally racy. + * + * The call to `cancel()` will try to cancel [auxFuture]: if and only if cancellation of [auxFuture] + * succeeds, [jobToCancel] will have its [Job.cancel] called. + * + * This arrangement means that [jobToCancel] _might not successfully cancel_, if the race resolves + * in a particular way. [jobToCancel] may also be in its "cancelling" state while this + * ListenableFuture is complete and cancelled. + */ + override fun cancel(mayInterruptIfRunning: Boolean): Boolean { + // TODO: call jobToCancel.cancel() _before_ running the listeners. + // `auxFuture.cancel()` will execute auxFuture's listeners. This delays cancellation of + // `jobToCancel` until after auxFuture's listeners have already run. + // Consider moving `jobToCancel.cancel()` into [AbstractFuture.afterDone] when the API is finalized. + return if (auxFuture.cancel(mayInterruptIfRunning)) { + jobToCancel.cancel() + true + } else { + false + } + } + + override fun toString(): String = buildString { + append(super.toString()) + append("[status=") + if (isDone) { + try { + when (val result = Uninterruptibles.getUninterruptibly(auxFuture)) { + is Cancelled -> append("CANCELLED, cause=[${result.exception}]") + else -> append("SUCCESS, result=[$result]") + } + } catch (e: CancellationException) { + // `this` future was cancelled by `Future.cancel`. In this case there's no cause or message. + append("CANCELLED") + } catch (e: ExecutionException) { + append("FAILURE, cause=[${e.cause}]") + } catch (t: Throwable) { + // Violation of Future's contract, should never happen. + append("UNKNOWN, cause=[${t.javaClass} thrown from get()]") + } + } else { + append("PENDING, delegate=[$auxFuture]") + } + append(']') + } +} + +/** + * A wrapper for `Coroutine`'s [CancellationException]. + * + * If the coroutine is _cancelled normally_, we want to show the reason of cancellation to the user. Unfortunately, + * [SettableFuture] can't store the reason of cancellation. To mitigate this, we wrap cancellation exception into this + * class and pass it into [SettableFuture.complete]. See implementation of [JobListenableFuture]. + */ +private class Cancelled(@JvmField val exception: CancellationException) diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml index c7c68f52754390..7d7828e8af111b 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml +++ b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml @@ -5,9 +5,8 @@ --> - io.gitpod.ide.jetbrains.backend - Gitpod - + io.gitpod.jetbrains.remote + Gitpod Remote Gitpod @@ -15,7 +14,7 @@ com.intellij.modules.platform - + diff --git a/components/ide/jetbrains/gateway-plugin/.gitignore b/components/ide/jetbrains/gateway-plugin/.gitignore new file mode 100644 index 00000000000000..f8baeaf8617bd2 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/.gitignore @@ -0,0 +1,4 @@ +.gradle +.idea +build +bin \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/.run/Run IDE with Plugin.run.xml b/components/ide/jetbrains/gateway-plugin/.run/Run IDE with Plugin.run.xml new file mode 100644 index 00000000000000..d15ff681a0ded7 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/.run/Run IDE with Plugin.run.xml @@ -0,0 +1,24 @@ + + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/.run/Run Plugin Tests.run.xml b/components/ide/jetbrains/gateway-plugin/.run/Run Plugin Tests.run.xml new file mode 100644 index 00000000000000..03d02876dfad42 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/.run/Run Plugin Tests.run.xml @@ -0,0 +1,24 @@ + + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/.run/Run Plugin Verification.run.xml b/components/ide/jetbrains/gateway-plugin/.run/Run Plugin Verification.run.xml new file mode 100644 index 00000000000000..b3b4eb8507abd2 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/.run/Run Plugin Verification.run.xml @@ -0,0 +1,24 @@ + + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/BUILD.yaml b/components/ide/jetbrains/gateway-plugin/BUILD.yaml new file mode 100644 index 00000000000000..0be7f9b122d463 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/BUILD.yaml @@ -0,0 +1,20 @@ +packages: + - name: publish + type: generic + deps: + - components/gitpod-protocol/java:lib + srcs: + - "gradle.properties" + - "gradle/wrapper/*" + - "gradlew" + - "src/*" + - "*.kts" + - "*.md" + env: + - JAVA_HOME=/home/gitpod/.sdkman/candidates/java/current + - DO_PUBLISH=${publishToJBMarketplace} + argdeps: + - jbMarketplacePublishTrigger + config: + commands: + - [ "./gradlew", "-PpluginVersion=0.0.1-${version}", "-PgitpodProtocolProjectPath=components-gitpod-protocol-java--lib/", "buildFromLeeway" ] diff --git a/components/ide/jetbrains/gateway-plugin/CHANGELOG.md b/components/ide/jetbrains/gateway-plugin/CHANGELOG.md new file mode 100644 index 00000000000000..4510da04df68be --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/CHANGELOG.md @@ -0,0 +1,9 @@ + + +# intellij-gateway-plugin Changelog + +## [Unreleased] +## [0.0.1] +## [0.0.1] +### Added +- Initial scaffold created from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template) diff --git a/components/ide/jetbrains/gateway-plugin/README.md b/components/ide/jetbrains/gateway-plugin/README.md new file mode 100644 index 00000000000000..bff4c38cc0d7a4 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/README.md @@ -0,0 +1,18 @@ +## Gitpod Gateway + + +Provides a way to connect to Gitpod workspaces. + + + +## Usage + +This project is not yet Gitpodified as testing it requires running the local Gateway app. For now, use IntelliJ idea. + +- Run `./gradlew runIde` to start a sandbox Gateway with the plugin installed +- Run `./gradlew check` to run the tests and the static analysis validations + +**Note**: Gradle should run with Java 11. + +--- +Plugin based on the [IntelliJ Platform Plugin Template][template]. diff --git a/components/ide/jetbrains/gateway-plugin/build.gradle.kts b/components/ide/jetbrains/gateway-plugin/build.gradle.kts new file mode 100644 index 00000000000000..2a008dfc46fa70 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/build.gradle.kts @@ -0,0 +1,147 @@ +// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +import io.gitlab.arturbosch.detekt.Detekt +import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +fun properties(key: String) = project.findProperty(key).toString() + +plugins { + // Java support + id("java") + // Kotlin support + id("org.jetbrains.kotlin.jvm") version "1.5.10" + // gradle-intellij-plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin + id("org.jetbrains.intellij") version "1.1.5" + // gradle-changelog-plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin + id("org.jetbrains.changelog") version "1.1.2" + // detekt linter - read more: https://detekt.github.io/detekt/gradle.html + id("io.gitlab.arturbosch.detekt") version "1.17.1" + // ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle + id("org.jlleitschuh.gradle.ktlint") version "10.0.0" +} + +group = properties("pluginGroup") +version = properties("pluginVersion") + +// Configure project's dependencies +repositories { + mavenCentral() +} +dependencies { + implementation(project(":gitpod-protocol")) { + artifact { + type = "jar" + } + } + compileOnly("javax.websocket:javax.websocket-api:1.1") + compileOnly("org.eclipse.jetty.websocket:websocket-api:9.4.44.v20210927") + testImplementation(kotlin("test")) + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.18.1") +} + +// Configure gradle-intellij-plugin plugin. +// Read more: https://github.com/JetBrains/gradle-intellij-plugin +intellij { + pluginName.set(properties("pluginName")) + version.set(properties("platformVersion")) + type.set(properties("platformType")) + downloadSources.set(properties("platformDownloadSources").toBoolean()) + updateSinceUntilBuild.set(true) + instrumentCode.set(false) + + // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. + plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) +} + +// Configure gradle-changelog-plugin plugin. +// Read more: https://github.com/JetBrains/gradle-changelog-plugin +changelog { + version = properties("pluginVersion") + groups = emptyList() +} + +// Configure detekt plugin. +// Read more: https://detekt.github.io/detekt/kotlindsl.html +detekt { + autoCorrect = true + buildUponDefaultConfig = true + + reports { + html.enabled = false + xml.enabled = false + txt.enabled = false + } +} + +tasks { + // Set the compatibility versions to 1.8 + withType { + sourceCompatibility = "11" + targetCompatibility = "11" + } + withType { + kotlinOptions.jvmTarget = "11" + kotlinOptions.freeCompilerArgs = listOf("-Xjvm-default=enable") + } + + withType { + jvmTarget = "11" + } + + test { + useJUnitPlatform() + } + + patchPluginXml { + version.set(properties("pluginVersion")) + sinceBuild.set(properties("pluginSinceBuild")) + untilBuild.set(properties("pluginUntilBuild")) + + // Extract the section from README.md and provide for the plugin's manifest + pluginDescription.set( + File(projectDir, "README.md").readText().lines().run { + val start = "" + val end = "" + + if (!containsAll(listOf(start, end))) { + throw GradleException("Plugin description section not found in README.md:\n$start ... $end") + } + subList(indexOf(start) + 1, indexOf(end)) + }.joinToString("\n").run { markdownToHTML(this) } + ) + + // Get the latest available change notes from the changelog file + changeNotes.set(provider { changelog.getLatest().toHTML() }) + } + + runPluginVerifier { + ideVersions.set(properties("pluginVerifierIdeVersions").split(',').map(String::trim).filter(String::isNotEmpty)) + } + + publishPlugin { + token.set(System.getenv("JB_MARKETPLACE_PUBLISH_TOKEN")) + // pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 + // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: + // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel + var pluginChannel: String? = System.getenv("JB_GATEWAY_GITPOD_PLUGIN_CHANNEL") + if (pluginChannel.isNullOrBlank()) { + pluginChannel = if (properties("pluginVersion").matches(".+-main\\..+".toRegex())) { + "Nightly" + } else { + "Dev" + } + } + channels.set(listOf(pluginChannel)) + } + + register("buildFromLeeway") { + if ("true" == System.getenv("DO_PUBLISH")) { + dependsOn("publishPlugin") + } else { + dependsOn("buildPlugin") + } + } +} diff --git a/components/ide/jetbrains/gateway-plugin/doc/installing.md b/components/ide/jetbrains/gateway-plugin/doc/installing.md new file mode 100644 index 00000000000000..20b235b06c5633 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/doc/installing.md @@ -0,0 +1,18 @@ +## Installing Gitpod plugin in JetBrains Gateway + + + +- Download the plugin archive: https://plugins.jetbrains.com/plugin/download?rel=true&updateId=154782 + - We are in the process of establishing update channels on JetBrains marketplace to enable auto upgrade. As for now + if something does not work please make sure to report an issue and check here for newer version of the plugin. +- Start JetBrains Gateway +- [Install from the plugin archive](https://www.jetbrains.com/help/idea/managing-plugins.html#install_plugin_from_disk) + - You may see an error about a missing dependency, ignore it. We are in process of sorting it out. +- Restart JetBrains Gateway diff --git a/components/ide/jetbrains/gateway-plugin/gradle.properties b/components/ide/jetbrains/gateway-plugin/gradle.properties new file mode 100644 index 00000000000000..247d6b3fa0a3b6 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/gradle.properties @@ -0,0 +1,23 @@ +# IntelliJ Platform Artifacts Repositories +# -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html +pluginGroup=io.gitpod.jetbrains +pluginName=gitpod-gateway +# It is overriden by CI during the build. +pluginVersion=0.0.1 +# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html +# for insight into build numbers and IntelliJ Platform versions. +pluginSinceBuild=213 +pluginUntilBuild=213.* +# Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl +# See https://jb.gg/intellij-platform-builds-list for available build versions. +pluginVerifierIdeVersions=2021.3.1 +platformType=GW +platformVersion=213.6777.25-CUSTOM-SNAPSHOT +platformDownloadSources=true +# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html +# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 +platformPlugins= +# Opt-out flag for bundling Kotlin standard library. +# See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. +kotlin.stdlib.default.dependency=false +gitpodProtocolProjectPath=../../../gitpod-protocol/java diff --git a/components/ide/jetbrains/gateway-plugin/gradle/wrapper/gradle-wrapper.jar b/components/ide/jetbrains/gateway-plugin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000000..e708b1c023ec8b Binary files /dev/null and b/components/ide/jetbrains/gateway-plugin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/components/ide/jetbrains/gateway-plugin/gradle/wrapper/gradle-wrapper.properties b/components/ide/jetbrains/gateway-plugin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000000..0f80bbf516ce01 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/components/ide/jetbrains/gateway-plugin/gradlew b/components/ide/jetbrains/gateway-plugin/gradlew new file mode 100755 index 00000000000000..4f906e0c811fc9 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/components/ide/jetbrains/gateway-plugin/gradlew.bat b/components/ide/jetbrains/gateway-plugin/gradlew.bat new file mode 100644 index 00000000000000..107acd32c4e687 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/components/ide/jetbrains/gateway-plugin/settings.gradle.kts b/components/ide/jetbrains/gateway-plugin/settings.gradle.kts new file mode 100644 index 00000000000000..024d877e048c8e --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/settings.gradle.kts @@ -0,0 +1,9 @@ +// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +rootProject.name = "jetbrains-gateway-gitpod-plugin" + +include(":gitpod-protocol") +val gitpodProtocolProjectPath: String by settings +project(":gitpod-protocol").projectDir = File(gitpodProtocolProjectPath) diff --git a/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/auth/GitpodAuthCallbackHandler.kt b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/auth/GitpodAuthCallbackHandler.kt new file mode 100644 index 00000000000000..503c573d9304ef --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/auth/GitpodAuthCallbackHandler.kt @@ -0,0 +1,60 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.auth + +import com.intellij.openapi.application.ApplicationManager +import io.netty.buffer.Unpooled +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.http.FullHttpRequest +import io.netty.handler.codec.http.QueryStringDecoder +import org.jetbrains.ide.RestService +import org.jetbrains.io.response +import java.nio.charset.StandardCharsets + +internal class GitpodAuthCallbackHandler : RestService() { + private val service = GitpodAuthService.instance + + override fun getServiceName(): String = service.name + + override fun execute( + urlDecoder: QueryStringDecoder, + request: FullHttpRequest, + context: ChannelHandlerContext + ): String? { + service.handleServerCallback(urlDecoder.path(), urlDecoder.parameters()) + sendResponse( + request, + context, + response("text/html", Unpooled.wrappedBuffer(responseHTML.toByteArray(StandardCharsets.UTF_8))) + ) + ApplicationManager.getApplication().invokeAndWait { + activateLastFocusedFrame() + } + return null + } + + companion object { + private val responseHTML = """ + + + + Done + + + + If this tab is not closed automatically, feel free to close it and proceed. + + + """.trimIndent() + } +} \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/auth/GitpodAuthService.kt b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/auth/GitpodAuthService.kt new file mode 100644 index 00000000000000..82c7890b3960a4 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/auth/GitpodAuthService.kt @@ -0,0 +1,205 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.auth + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.intellij.collaboration.auth.credentials.Credentials +import com.intellij.collaboration.auth.credentials.SimpleCredentials +import com.intellij.collaboration.auth.services.OAuthCredentialsAcquirer +import com.intellij.collaboration.auth.services.OAuthCredentialsAcquirerHttp +import com.intellij.collaboration.auth.services.OAuthRequest +import com.intellij.collaboration.auth.services.OAuthServiceBase +import com.intellij.credentialStore.CredentialAttributes +import com.intellij.credentialStore.generateServiceName +import com.intellij.ide.passwordSafe.PasswordSafe +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.util.Base64 +import com.intellij.util.EventDispatcher +import com.intellij.util.Url +import com.intellij.util.Urls.encodeParameters +import com.intellij.util.Urls.newFromEncoded +import com.intellij.util.io.DigestUtil +import kotlinx.coroutines.future.await +import org.jetbrains.ide.BuiltInServerManager +import org.jetbrains.ide.RestService +import java.io.IOException +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.security.SecureRandom +import java.util.* +import java.util.concurrent.CompletableFuture +import kotlin.math.absoluteValue + + +@Service +internal class GitpodAuthService : OAuthServiceBase() { + override val name: String + get() = SERVICE_NAME + + fun authorize(gitpodHost: String): CompletableFuture { + return authorize(GitpodAuthRequest(gitpodHost)) + } + + override fun revokeToken(token: String) { + throw Exception("Not yet implemented") + } + + private class GitpodAuthRequest : OAuthRequest { + + private val port = BuiltInServerManager.getInstance().port + + override val authorizationCodeUrl = + newFromEncoded("http://127.0.0.1:$port/${RestService.PREFIX}/$SERVICE_NAME/authorization_code") + + override val credentialsAcquirer: OAuthCredentialsAcquirer + + override val authUrlWithParameters: Url + + constructor(gitpodHost: String) { + val codeVerifier = generateCodeVerifier() + val codeChallenge = generateCodeChallenge(codeVerifier) + val serviceUrl = newFromEncoded("https://${gitpodHost}/api/oauth") + credentialsAcquirer = GitpodAuthCredentialsAcquirer( + serviceUrl.resolve("token"), mapOf( + "grant_type" to "authorization_code", + "client_id" to CLIENT_ID, + "redirect_uri" to authorizationCodeUrl.toExternalForm(), + "code_verifier" to codeVerifier + ) + ) + authUrlWithParameters = serviceUrl.resolve("authorize").addParameters( + mapOf( + "client_id" to CLIENT_ID, + "redirect_uri" to authorizationCodeUrl.toExternalForm(), + "scope" to scopes.joinToString(" "), + "response_type" to "code", + "code_challenge" to codeChallenge, + "code_challenge_method" to "S256" + ) + ) + } + + companion object { + fun generateCodeVerifier(): String { + val size = 128 + val secureRandom = SecureRandom() + val bytes = ByteArray(size) + secureRandom.nextBytes(bytes) + + val mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"; + val scale = 256 / mask.length + val builder = StringBuilder() + for (i in 0 until size) { + builder.append(mask[bytes[i].floorDiv(scale).absoluteValue]) + } + return builder.toString() + } + + fun generateCodeChallenge(codeVerifier: String): String { + val hash = DigestUtil.sha256().digest(codeVerifier.toByteArray()) + return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(hash) + } + } + } + + private class GitpodAuthCredentialsAcquirer( + private val tokenUrl: Url, + private val parameters: Map + ) : OAuthCredentialsAcquirer { + override fun acquireCredentials(code: String): OAuthCredentialsAcquirer.AcquireCredentialsResult { + val response = try { + val parameters = HashMap(parameters) + parameters["code"] = code + val bodyBuilder = StringBuilder() + encodeParameters(parameters, bodyBuilder) + val body = bodyBuilder.toString() + val client = HttpClient.newHttpClient() + val request = HttpRequest.newBuilder() + .uri(URI.create(tokenUrl.toExternalForm())) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build() + client.send(request, HttpResponse.BodyHandlers.ofString()) + } catch (e: IOException) { + return OAuthCredentialsAcquirer.AcquireCredentialsResult.Error("Cannot exchange token: ${e.message}") + } + return OAuthCredentialsAcquirerHttp.convertToAcquireCredentialsResult(response) { body, _ -> + val responseData = with(jacksonMapper) { + propertyNamingStrategy = PropertyNamingStrategies.SnakeCaseStrategy() + readValue(body, AuthorizationResponseData::class.java) + } + + val jwt = with(jacksonMapper) { + propertyNamingStrategy = PropertyNamingStrategies.LowerCaseStrategy() + readValue(Base64.decode(responseData.accessToken.split('.')[1]), JsonWebToken::class.java) + } + SimpleCredentials(jwt.jti) + } + } + + private data class AuthorizationResponseData(val accessToken: String) + private data class JsonWebToken(val jti: String) + + } + + companion object { + val instance: GitpodAuthService = service() + + private const val SERVICE_NAME = "gitpod/oauth" + private const val CLIENT_ID = "jetbrains-gateway-gitpod-plugin" + val scopes = arrayOf( + "function:getGitpodTokenScopes", + "function:getIDEOptions", + "function:getOwnerToken", + "function:getWorkspace", + "function:getWorkspaces", + "function:listenForWorkspaceInstanceUpdates", + "resource:default" + ) + private val jacksonMapper = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + fun hasAccessToken(gitpodHost: String) = + getAccessToken(gitpodHost) != null + + fun getAccessToken(gitpodHost: String) = + PasswordSafe.instance.getPassword(getAccessTokenCredentialAttributes(gitpodHost)) + + fun setAccessToken(gitpodHost: String, accessToken: String?) { + PasswordSafe.instance.setPassword(getAccessTokenCredentialAttributes(gitpodHost), accessToken) + dispatcher.multicaster.didChange() + } + + suspend fun authorize(gitpodHost: String): String { + val accessToken = instance.authorize(gitpodHost).await().accessToken + setAccessToken(gitpodHost, accessToken!!) + return accessToken + } + + private fun getAccessTokenCredentialAttributes(gitpodHost: String) = + CredentialAttributes(generateServiceName("Gitpod", gitpodHost)) + + private interface Listener : EventListener { + fun didChange() + } + + private val dispatcher = EventDispatcher.create(Listener::class.java) + fun addListener(listener: () -> Unit): Disposable { + val internalListener = object : Listener { + override fun didChange() { + listener() + } + } + dispatcher.addListener(internalListener); + return Disposable { dispatcher.removeListener(internalListener) } + } + } +} \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GatewayGitpodClient.kt b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GatewayGitpodClient.kt new file mode 100644 index 00000000000000..b6e8b2986b8e5e --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GatewayGitpodClient.kt @@ -0,0 +1,141 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.gateway + +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.remoteDev.util.onTerminationOrNow +import com.jetbrains.rd.util.CopyOnWriteArrayList +import com.jetbrains.rd.util.concurrentMapOf +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import io.gitpod.gitpodprotocol.api.GitpodClient +import io.gitpod.gitpodprotocol.api.entities.WorkspaceInfo +import io.gitpod.gitpodprotocol.api.entities.WorkspaceInstance +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.future.await +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class GatewayGitpodClient( + private val lifetimeDefinition: LifetimeDefinition, private val gitpodHost: String +) : GitpodClient() { + + private val mutex = Mutex() + + private val listeners = concurrentMapOf>?>() + + private val timeoutDelayInMinutes = 15 + private var timeoutJob: Job? = null; + + init { + GlobalScope.launch { + mutex.withLock { + scheduleTimeout("waiting for workspace listeners") + } + } + } + + private fun scheduleTimeout(reason: String) { + if (timeoutJob?.isActive == true) { + return + } + timeoutJob = GlobalScope.launch { + thisLogger().info("$gitpodHost: connection times out in $timeoutDelayInMinutes minutes: $reason") + delay(timeoutDelayInMinutes * 60 * 1000L) + if (isActive) { + lifetimeDefinition.terminate() + } + } + } + + private fun cancelTimeout(reason: String) { + if (timeoutJob?.isActive == true) { + thisLogger().info("$gitpodHost: canceled connection timeout: $reason") + timeoutJob!!.cancel() + } + } + + private var syncJob: Job? = null; + override fun notifyConnect() { + syncJob?.cancel() + syncJob = GlobalScope.launch { + for (id in listeners.keys) { + ensureActive() + if (id == "*") { + continue + } + try { + syncWorkspace(id); + } catch (t: Throwable) { + thisLogger().error("${gitpodHost}: ${id}: failed to sync", t) + } + } + } + } + + override fun onInstanceUpdate(instance: WorkspaceInstance?) { + if (instance == null) { + return; + } + GlobalScope.launch { + val wsListeners = listeners[instance.workspaceId] ?: return@launch + for (listener in wsListeners) { + listener.send(instance) + } + } + GlobalScope.launch { + val anyListeners = listeners["*"] ?: return@launch + for (listener in anyListeners) { + listener.send(instance) + } + } + } + + suspend fun listenToWorkspace( + listenerLifetime: Lifetime, + workspaceId: String + ): ReceiveChannel { + val listener = Channel() + mutex.withLock { + val listeners = this.listeners.getOrPut(workspaceId) { CopyOnWriteArrayList() }!! + listeners.add(listener); + cancelTimeout("listening to workspace: $workspaceId") + } + listenerLifetime.onTerminationOrNow { + listener.close() + GlobalScope.launch { + removeListener(workspaceId, listener) + } + } + return listener + } + + private suspend fun removeListener(workspaceId: String, listener: Channel) { + mutex.withLock { + val listeners = this.listeners[workspaceId] + if (listeners.isNullOrEmpty()) { + return + } + listeners.remove(listener); + if (listeners.isNotEmpty()) { + return + } + this.listeners.remove(workspaceId) + if (this.listeners.isNotEmpty()) { + return + } + scheduleTimeout("no workspace listeners") + } + } + + suspend fun syncWorkspace(id: String): WorkspaceInfo { + val info = server.getWorkspace(id).await() + onInstanceUpdate(info.latestInstance) + return info + } + +} \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodConnectionProvider.kt b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodConnectionProvider.kt new file mode 100644 index 00000000000000..487b19b74232d6 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodConnectionProvider.kt @@ -0,0 +1,290 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.gateway + +import com.intellij.ide.BrowserUtil +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.remote.RemoteCredentialsHolder +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.gridLayout.HorizontalAlign +import com.intellij.ui.dsl.gridLayout.VerticalAlign +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import com.jetbrains.gateway.api.ConnectionRequestor +import com.jetbrains.gateway.api.GatewayConnectionHandle +import com.jetbrains.gateway.api.GatewayConnectionProvider +import com.jetbrains.gateway.ssh.ClientOverSshTunnelConnector +import com.jetbrains.gateway.thinClientLink.ThinClientHandle +import com.jetbrains.rd.util.URI +import com.jetbrains.rd.util.lifetime.Lifetime +import io.gitpod.gitpodprotocol.api.entities.WorkspaceInstance +import io.gitpod.jetbrains.icons.GitpodIcons +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.net.URL +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.time.Duration +import javax.swing.JComponent +import javax.swing.JLabel + +class GitpodConnectionProvider : GatewayConnectionProvider { + + private val gitpod = service() + + override suspend fun connect( + parameters: Map, + requestor: ConnectionRequestor + ): GatewayConnectionHandle? { + if (parameters["gitpodHost"] == null) { + throw IllegalArgumentException("bad gitpodHost parameter"); + } + if (parameters["workspaceId"] == null) { + throw IllegalArgumentException("bad workspaceId parameter"); + } + val connectParams = ConnectParams( + parameters["gitpodHost"]!!, + parameters["workspaceId"]!! + ) + val client = gitpod.obtainClient(connectParams.gitpodHost) + val connectionLifetime = Lifetime.Eternal.createNested() + val updates = client.listenToWorkspace(connectionLifetime, connectParams.workspaceId) + val workspace = client.syncWorkspace(connectParams.workspaceId).workspace + + val phaseMessage = JLabel() + val statusMessage = JLabel() + val errorMessage = JLabel() + var ideUrl = ""; + val connectionPanel = panel { + row { + resizableRow() + panel { + resizableColumn() + verticalAlign(VerticalAlign.CENTER) + row { + icon(GitpodIcons.Logo2x) + .horizontalAlign(HorizontalAlign.CENTER) + } + row { + cell(phaseMessage) + .bold() + .horizontalAlign(HorizontalAlign.CENTER) + } + row { + cell(statusMessage) + .horizontalAlign(HorizontalAlign.CENTER) + .applyToComponent { + foreground = JBUI.CurrentTheme.ContextHelp.FOREGROUND + } + } + panel { + row { + link(connectParams.workspaceId) { + if (ideUrl.isNotBlank()) { + BrowserUtil.browse(ideUrl) + } + } + } + row { + browserLink(workspace.context.normalizedContextURL, workspace.context.normalizedContextURL) + } + }.horizontalAlign(HorizontalAlign.CENTER) + row { + cell(errorMessage) + .horizontalAlign(HorizontalAlign.CENTER) + .applyToComponent { + foreground = UIUtil.getErrorForeground() + } + } + } + } + } + + GlobalScope.launch { + var thinClient: ThinClientHandle? = null; + var thinClientJob: Job? = null; + + val httpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .build() + + var lastUpdate: WorkspaceInstance? = null; + try { + for (update in updates) { + try { + if (WorkspaceInstance.isUpToDate(lastUpdate, update)) { + continue; + } + ideUrl = update.ideUrl + lastUpdate = update; + if (!update.status.conditions.failed.isNullOrBlank()) { + errorMessage.text = update.status.conditions.failed; + } + when (update.status.phase) { + "preparing" -> { + phaseMessage.text = "Preparing" + statusMessage.text = "Building workspace image..." + } + "pending" -> { + phaseMessage.text = "Preparing" + statusMessage.text = "Allocating resources …" + } + "creating" -> { + phaseMessage.text = "Creating" + statusMessage.text = "Pulling workspace image …" + } + "initializing" -> { + phaseMessage.text = "Starting" + statusMessage.text = "Initializing workspace content …" + } + "running" -> { + phaseMessage.text = "Running" + statusMessage.text = "Connecting..." + } + "interrupted" -> { + phaseMessage.text = "Starting" + statusMessage.text = "Checking workspace …" + } + "stopping" -> { + phaseMessage.text = "Stopping" + statusMessage.text = "" + } + "stopped" -> { + if (update.status.conditions.timeout.isNullOrBlank()) { + phaseMessage.text = "Stopped" + } else { + phaseMessage.text = "Timed Out" + } + statusMessage.text = "" + } + else -> { + phaseMessage.text = "" + statusMessage.text = "" + } + } + + if (update.status.phase == "stopping" || update.status.phase == "stopped") { + thinClientJob?.cancel() + thinClient?.close() + } + + if (thinClientJob == null && update.status.phase == "running") { + thinClientJob = launch { + val ownerToken = client.server.getOwnerToken(update.workspaceId).await() + + val ideUrl = URL(update.ideUrl); + var joinLink: String? = null + val maxRequestTimeout = 30 * 1000L + val timeoutDelayGrowFactor = 1.5; + var requestTimeout = 2 * 1000L + while (joinLink == null) { + try { + val httpRequest = HttpRequest.newBuilder() + .uri(URI.create("https://24000-${ideUrl.host}/joinLink")) + .header("x-gitpod-owner-token", ownerToken) + .GET() + .timeout(Duration.ofMillis(requestTimeout)) + .build() + val response = + httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()) + if (response.statusCode() == 200) { + joinLink = response.body() + errorMessage.text = "" + } else { + errorMessage.text = + "failed to fetch join link: ${response.statusCode()}, trying again..."; + } + } catch (t: Throwable) { + if (t is CancellationException) { + throw t + } + thisLogger().error( + "${connectParams.gitpodHost}: ${connectParams.workspaceId}: failed to fetch join link:", + t + ) + errorMessage.text = "failed to fetch join link: ${t.message}, trying again..."; + } + requestTimeout = (requestTimeout * timeoutDelayGrowFactor).toLong() + if (requestTimeout > maxRequestTimeout) { + requestTimeout = maxRequestTimeout + } + } + + val credentials = RemoteCredentialsHolder() + credentials.setHost(ideUrl.host) + credentials.port = 22 + credentials.userName = update.workspaceId + credentials.password = ownerToken + + val connector = ClientOverSshTunnelConnector( + connectionLifetime, + credentials, + URI(joinLink) + ) + val client = connector.connect() + client.clientClosed.advise(connectionLifetime) { + connectionLifetime.terminate() + } + client.onClientPresenceChanged.advise(connectionLifetime) { + if (client.clientPresent) { + statusMessage.text = "" + } + } + thinClient = client + } + } + } catch (e: Throwable) { + thisLogger().error( + "${connectParams.gitpodHost}: ${connectParams.workspaceId}: failed to process workspace update:", + e + ) + } + } + connectionLifetime.terminate() + } catch (t: Throwable) { + thisLogger().error( + "${connectParams.gitpodHost}: ${connectParams.workspaceId}: failed to process workspace updates:", + t + ) + errorMessage.text = " failed to process workspace updates ${t.message}" + } + } + + return GitpodConnectionHandle(connectionLifetime, connectionPanel, connectParams); + } + + override fun isApplicable(parameters: Map): Boolean = + parameters.containsKey("gitpodHost") + + private data class ConnectParams( + val gitpodHost: String, + val workspaceId: String + ) + + private class GitpodConnectionHandle( + lifetime: Lifetime, + private val component: JComponent, + private val params: ConnectParams + ) : GatewayConnectionHandle(lifetime) { + + override fun createComponent(): JComponent { + return component + } + + override fun getTitle(): String { + return "${params.workspaceId} (${params.gitpodHost})" + } + + override fun hideToTrayOnStart(): Boolean { + return false + } + } + +} diff --git a/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodConnectionService.kt b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodConnectionService.kt new file mode 100644 index 00000000000000..e0e73624a6ecbc --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodConnectionService.kt @@ -0,0 +1,126 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.gateway + +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.components.Service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.extensions.PluginId +import com.intellij.remoteDev.util.onTerminationOrNow +import com.intellij.util.ExceptionUtil +import com.intellij.util.io.DigestUtil +import com.jetbrains.rd.util.concurrentMapOf +import com.jetbrains.rd.util.lifetime.Lifetime +import io.gitpod.gitpodprotocol.api.GitpodServerLauncher +import io.gitpod.jetbrains.auth.GitpodAuthService +import kotlinx.coroutines.* +import kotlinx.coroutines.future.await +import org.eclipse.jetty.websocket.api.UpgradeException +import java.nio.charset.StandardCharsets + +@Service +class GitpodConnectionService { + + private val clients = concurrentMapOf(); + + fun obtainClient(gitpodHost: String): GatewayGitpodClient { + return clients.getOrPut(gitpodHost) { + val lifetime = Lifetime.Eternal.createNested() + val client = GatewayGitpodClient(lifetime, gitpodHost) + val launcher = GitpodServerLauncher.create(client) + val job = GlobalScope.launch { + var accessToken = GitpodAuthService.getAccessToken(gitpodHost) + val authorize = suspend { + ensureActive() + accessToken = GitpodAuthService.authorize(gitpodHost) + } + if (accessToken == null) { + authorize(); + } + + val plugin = PluginManagerCore.getPlugin(PluginId.getId("io.gitpod.jetbrains.gateway"))!! + val connect = suspend { + ensureActive() + val originalClassLoader = Thread.currentThread().contextClassLoader + val connection = try { + // see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003146180/comments/360000376240 + Thread.currentThread().contextClassLoader = GitpodConnectionProvider::class.java.classLoader + launcher.listen( + "wss://${gitpodHost}/api/v1", + "https://${gitpodHost}/", + plugin.pluginId.idString, + plugin.version, + accessToken + ) + } catch (t: Throwable) { + val badUpgrade = ExceptionUtil.findCause(t, UpgradeException::class.java) + if (badUpgrade?.responseStatusCode == 401 || badUpgrade?.responseStatusCode == 403) { + throw InvalidTokenException("failed web socket handshake (${badUpgrade.responseStatusCode})") + } + throw t + } finally { + Thread.currentThread().contextClassLoader = originalClassLoader; + } + val tokenHash = DigestUtil.sha256Hex(accessToken!!.toByteArray(StandardCharsets.UTF_8)) + val tokenScopes = client.server.getGitpodTokenScopes(tokenHash).await() + for (scope in GitpodAuthService.scopes) { + if (!tokenScopes.contains(scope)) { + connection.cancel(false) + throw InvalidTokenException("$scope scope is not granted") + } + } + connection + } + + val minReconnectionDelay = 2 * 1000L + val maxReconnectionDelay = 30 * 1000L + val reconnectionDelayGrowFactor = 1.5; + var reconnectionDelay = minReconnectionDelay; + while (isActive) { + try { + val connection = try { + connect() + } catch (t: Throwable) { + val e = ExceptionUtil.findCause(t, InvalidTokenException::class.java) ?: throw t + thisLogger().warn("${gitpodHost}: invalid token, authorizing again and reconnecting:", e) + authorize() + connect() + } + reconnectionDelay = minReconnectionDelay + thisLogger().info("${gitpodHost}: connected") + val reason = connection.await() + if (isActive) { + thisLogger().warn("${gitpodHost}: connection closed, reconnecting after $reconnectionDelay milliseconds: $reason") + } else { + thisLogger().info("${gitpodHost}: connection permanently closed: $reason") + } + } catch (t: Throwable) { + if (isActive) { + thisLogger().warn( + "${gitpodHost}: failed to connect, trying again after $reconnectionDelay milliseconds:", + t + ) + } else { + thisLogger().error("${gitpodHost}: connection permanently closed:", t) + } + } + delay(reconnectionDelay) + reconnectionDelay = (reconnectionDelay * reconnectionDelayGrowFactor).toLong() + if (reconnectionDelay > maxReconnectionDelay) { + reconnectionDelay = maxReconnectionDelay + } + } + } + lifetime.onTerminationOrNow { + clients.remove(gitpodHost) + job.cancel() + } + return@getOrPut client + } + } + + private class InvalidTokenException(message: String) : Exception(message) + +} \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodConnector.kt b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodConnector.kt new file mode 100644 index 00000000000000..9bc32d2a576eb4 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodConnector.kt @@ -0,0 +1,505 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.gateway + +import com.intellij.icons.AllIcons +import com.intellij.ide.BrowserUtil +import com.intellij.openapi.CompositeDisposable +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeScreenUIManager +import com.intellij.remoteDev.util.onTerminationOrNow +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.gridLayout.HorizontalAlign +import com.intellij.ui.dsl.gridLayout.VerticalAlign +import com.intellij.ui.layout.ComponentPredicate +import com.intellij.ui.layout.not +import com.intellij.util.EventDispatcher +import com.intellij.util.ui.JBFont +import com.jetbrains.gateway.api.GatewayConnector +import com.jetbrains.gateway.api.GatewayConnectorView +import com.jetbrains.gateway.api.GatewayRecentConnections +import com.jetbrains.gateway.api.GatewayUI.Companion.getInstance +import com.jetbrains.rd.util.concurrentMapOf +import com.jetbrains.rd.util.lifetime.Lifetime +import com.jetbrains.rd.util.lifetime.LifetimeDefinition +import com.jetbrains.rd.util.lifetime.isAlive +import com.jetbrains.rd.util.lifetime.isNotAlive +import io.gitpod.gitpodprotocol.api.entities.GetWorkspacesOptions +import io.gitpod.gitpodprotocol.api.entities.IDEOption +import io.gitpod.gitpodprotocol.api.entities.WorkspaceInstance +import io.gitpod.jetbrains.auth.GitpodAuthService +import io.gitpod.jetbrains.icons.GitpodIcons +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.actor +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import java.awt.Component +import java.util.* +import javax.swing.DefaultComboBoxModel +import javax.swing.Icon +import javax.swing.JComponent +import javax.swing.text.SimpleAttributeSet +import javax.swing.text.StyleConstants +import javax.swing.text.StyledDocument + + +class GitpodConnector : GatewayConnector { + override val icon: Icon + get() = GitpodIcons.Logo + + override fun createView(lifetime: Lifetime): GatewayConnectorView { + return GitpodConnectorView(lifetime) + } + + override fun getActionText(): String { + return "Connect to Gitpod" + } + + override fun getDescription(): String? { + return "Connect to Gitpod workspaces" + } + + override fun getDocumentationLink(): String { + // TODO(ak) something JetBrains specific + return "https://www.gitpod.io/docs" + } + + override fun getDocumentationLinkText(): String { + return super.getDocumentationLinkText() + } + + override fun getRecentConnections(setContentCallback: (Component) -> Unit): GatewayRecentConnections? { + return GitpodRecentConnections(setContentCallback) + } + + override fun getTitle(): String { + return "Gitpod" + } + + override fun getTitleAdornment(): JComponent? { + return null + } + + override fun initProcedure() {} + + interface Listener : EventListener { + fun stateChanged() + } + + class GitpodConnectorView( + lifetime: Lifetime + ) : GatewayConnectorView { + private val settings = service() + + private val backendsModel = DefaultComboBoxModel() + private val backendToId = concurrentMapOf() + private val backendsLoadedDispatcher = EventDispatcher.create(Listener::class.java) + private val backendsLoaded = object : ComponentPredicate() { + + override fun addListener(listener: (Boolean) -> Unit) { + backendsLoadedDispatcher.addListener(object : Listener { + override fun stateChanged() { + listener(invoke()) + } + }) + } + + override fun invoke(): Boolean { + return backendsModel.size > 0 + } + } + + override val component = panel { + indent { + row { + icon(GitpodIcons.Logo).gap(RightGap.SMALL) + label("Gitpod").applyToComponent { + this.font = JBFont.h3().asBold() + } + }.topGap(TopGap.MEDIUM).bottomGap(BottomGap.SMALL) + row { + text("Gitpod is an open-source Kubernetes application for automated and ready-to-code development environments that blends in your existing workflow. It enables you to describe your dev environment as code and start instant and fresh development environments for each new task directly from your browser.") + } + row { + text("Tightly integrated with GitLab, GitHub, and Bitbucket, Gitpod automatically and continuously prebuilds dev environments for all your branches. As a result, team members can instantly start coding with fresh, ephemeral and fully-compiled dev environments - no matter if you are building a new feature, want to fix a bug or do a code review.") + } + row { + browserLink("Explore Gitpod", "https://www.gitpod.io/") + } + row { + label("Start from any GitLab, GitHub or Bitbucket URL:") + }.topGap(TopGap.MEDIUM) + row { + comboBox(backendsModel) + .gap(RightGap.SMALL) + .visibleIf(backendsLoaded) + val contextUrl = textField() + .resizableColumn() + .horizontalAlign(HorizontalAlign.FILL) + .applyToComponent { + this.text = "https://github.com/gitpod-io/spring-petclinic" + } + button("New Workspace") { + // TODO(ak) disable button if blank + if (contextUrl.component.text.isNotBlank()) { + val backend = backendsModel.selectedItem + val selectedBackendId = if (backend != null) { + backendToId[backend] + } else null + val backendParam = if (selectedBackendId != null) { + ":$selectedBackendId" + } else { + "" + } + BrowserUtil.browse("https://${settings.gitpodHost}#referrer:jetbrains-gateway$backendParam/${contextUrl.component.text}") + } + } + cell() + }.topGap(TopGap.NONE) + } + row { + resizableRow() + panel { + verticalAlign(VerticalAlign.BOTTOM) + separator(null, WelcomeScreenUIManager.getSeparatorColor()) + indent { + row { + button("Back") { + getInstance().reset() + } + } + } + } + }.bottomGap(BottomGap.SMALL) + }.apply { + this.background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() + } + + init { + val updatesJob = Job() + val updates = GlobalScope.actor(updatesJob, capacity = Channel.CONFLATED) { + for (event in channel) { + ensureActive() + + val gitpodHost = settings.gitpodHost + if (!GitpodAuthService.hasAccessToken(gitpodHost)) { + backendsModel.removeAllElements() + backendToId.clear() + } else { + val client = service().obtainClient(gitpodHost) + val ideOptions = client.server.ideOptions.await() + ensureActive() + + val toRemove = HashSet(backendToId.keys) + val clientOptions = ideOptions.clients?.get("jetbrains-gateway") + if (clientOptions?.desktopIDEs != null) { + for (backendId in clientOptions.desktopIDEs) { + val option = ideOptions.options[backendId] + if (option != null) { + toRemove.remove(option.title) + backendsModel.addElement(option.title) + backendToId[option.title] = backendId + } + } + } + for (title in toRemove) { + backendsModel.removeElement(title) + backendToId.remove(title) + } + + var selectedOption: IDEOption? = null + // TODO(ak) apply user option from settings + if (clientOptions?.defaultDesktopIDE != null) { + selectedOption = ideOptions.options[clientOptions.defaultDesktopIDE] + } + if (selectedOption != null) { + backendsModel.selectedItem = selectedOption.title + } + } + backendsLoadedDispatcher.multicaster.stateChanged() + } + } + lifetime.onTerminationOrNow { + updatesJob.cancel() + updates.close() + } + fun update() { + updates.trySend(null) + } + + update() + val toDispose = CompositeDisposable() + toDispose.add(settings.addListener { update() }) + toDispose.add(GitpodAuthService.addListener { update() }) + lifetime.onTerminationOrNow { toDispose.dispose() } + } + + } + + private class GitpodRecentConnections( + val setContentCallback: (Component) -> Unit + ) : GatewayRecentConnections { + + private val settings = service() + + override val recentsIcon = GitpodIcons.Logo + + private lateinit var scheduleUpdate: () -> Unit + override fun createRecentsView(lifetime: Lifetime): JComponent { + val loggedIn = object : ComponentPredicate() { + override fun addListener(listener: (Boolean) -> Unit) { + val toDispose = CompositeDisposable() + toDispose.add(settings.addListener { listener(invoke()) }) + toDispose.add(GitpodAuthService.addListener { listener(invoke()) }) + lifetime.onTerminationOrNow { toDispose.dispose() } + } + + override fun invoke(): Boolean { + return GitpodAuthService.hasAccessToken(settings.gitpodHost) + } + } + lateinit var workspacesPane: JBScrollPane + val view = panel { + indent { + row { + label("Gitpod Workspaces").applyToComponent { + this.font = JBFont.h3().asBold() + } + }.topGap(TopGap.MEDIUM).bottomGap(BottomGap.SMALL) + + row { + panel { + verticalAlign(VerticalAlign.CENTER) + for (i in 1..10) { + row { + label("") + } + } + row { + resizableRow() + icon(GitpodIcons.Logo4x) + .horizontalAlign(HorizontalAlign.CENTER) + } + row { + text( + "Spin up fresh, automated dev environments for each task, in the cloud, in seconds.", + 35 + ).applyToComponent { + val attrs = SimpleAttributeSet() + StyleConstants.setAlignment(attrs, StyleConstants.ALIGN_CENTER) + (document as StyledDocument).setParagraphAttributes( + 0, + document.length - 1, + attrs, + false + ) + }.horizontalAlign(HorizontalAlign.CENTER) + } + row { + browserLink("Explore Gitpod", "https://www.gitpod.io") + .horizontalAlign(HorizontalAlign.CENTER) + }.bottomGap(BottomGap.MEDIUM) + row { + button("Connect") { + GlobalScope.launch { + GitpodAuthService.authorize(settings.gitpodHost) + } + }.horizontalAlign(HorizontalAlign.CENTER) + } + } + }.visibleIf(loggedIn.not()) + + rowsRange { + row { + link("Open Dashboard") { + BrowserUtil.browse("https://${settings.gitpodHost}") + } + label("").resizableColumn().horizontalAlign(HorizontalAlign.FILL) + actionButton(object : + DumbAwareAction("New Workspace", "Create a new workspace", AllIcons.General.Add) { + override fun actionPerformed(e: AnActionEvent) { + val connectorView = GitpodConnectorView(lifetime.createNested()) + setContentCallback(connectorView.component) + } + }).gap(RightGap.SMALL) + actionButton(object : + DumbAwareAction("Refresh", "Refresh recent workspaces", AllIcons.Actions.Refresh) { + override fun actionPerformed(e: AnActionEvent) { + scheduleUpdate() + } + }) + cell() + } + row { + resizableRow() + workspacesPane = cell(JBScrollPane()) + .resizableColumn() + .horizontalAlign(HorizontalAlign.FILL) + .verticalAlign(VerticalAlign.FILL) + .component + cell() + } + row { + label("").resizableColumn().horizontalAlign(HorizontalAlign.FILL) + button("Logout") { + GitpodAuthService.setAccessToken(settings.gitpodHost, null) + } + cell() + }.topGap(TopGap.SMALL).bottomGap(BottomGap.SMALL) + }.visibleIf(loggedIn) + } + }.apply { + this.background = WelcomeScreenUIManager.getMainAssociatedComponentBackground() + } + this.scheduleUpdate = startUpdateLoop(lifetime, workspacesPane) + + scheduleUpdate() + loggedIn.addListener { scheduleUpdate() } + return view + } + + override fun getRecentsTitle(): String { + return "Gitpod" + } + + override fun updateRecentView() { + if (this::scheduleUpdate.isInitialized) { + scheduleUpdate() + } + } + + private fun startUpdateLoop(lifetime: Lifetime, workspacesPane: JBScrollPane): () -> Unit { + val updateJob = Job() + lifetime.onTerminationOrNow { updateJob.cancel() } + + val updateActor = GlobalScope.actor(updateJob, capacity = Channel.CONFLATED) { + var updateLifetime: LifetimeDefinition? = null + for (event in channel) { + ensureActive() + updateLifetime?.terminate() + updateLifetime = lifetime.createNested() + doUpdate(updateLifetime, workspacesPane); + } + } + lifetime.onTerminationOrNow { updateActor.close() } + + return { updateActor.trySend(null) } + } + + private fun doUpdate(updateLifetime: Lifetime, workspacesPane: JBScrollPane) { + val gitpodHost = settings.gitpodHost + if (!GitpodAuthService.hasAccessToken(gitpodHost)) { + ApplicationManager.getApplication().invokeLater { + if (updateLifetime.isAlive) { + workspacesPane.viewport.view = panel { + row { + comment("Loading...") + } + } + } + } + return + } + val job = GlobalScope.launch { + val client = service().obtainClient(gitpodHost) + val workspaces = client.server.getWorkspaces(GetWorkspacesOptions().apply { + this.limit = 20 + }).await() + val workspacesMap = workspaces.associateBy { it.workspace.id }.toMutableMap() + fun updateView() { + val view = panel { + for (info in workspacesMap.values) { + if (info.latestInstance == null) { + continue; + } + indent { + row { + var canConnect = false + icon( + if (info.latestInstance.status.phase == "running") { + canConnect = true + GitpodIcons.Running + } else if (info.latestInstance.status.phase == "stopped") { + if (info.latestInstance.status.conditions.failed.isNullOrBlank()) { + GitpodIcons.Stopped + } else { + GitpodIcons.Failed + } + } else if (info.latestInstance.status.phase == "interrupted") { + GitpodIcons.Failed + } else if (info.latestInstance.status.phase == "unknown") { + GitpodIcons.Failed + } else { + canConnect = true + GitpodIcons.Starting + } + ).gap(RightGap.SMALL) + panel { + row { + browserLink(info.workspace.id, info.latestInstance.ideUrl) + }.rowComment("${info.workspace.context.normalizedContextURL}") + } + label("").resizableColumn().horizontalAlign(HorizontalAlign.FILL) + button("Connect") { + if (!canConnect) { + BrowserUtil.browse(info.latestInstance.ideUrl) + } else { + getInstance().connect( + mapOf( + "gitpodHost" to gitpodHost, + "workspaceId" to info.workspace.id + ) + ) + } + } + cell() + } + } + } + } + ApplicationManager.getApplication().invokeLater { + if (updateLifetime.isAlive) { + workspacesPane.viewport.view = view + } + } + } + updateView() + val updates = client.listenToWorkspace(updateLifetime, "*") + for (update in updates) { + if (updateLifetime.isNotAlive) { + return@launch + } + var info = workspacesMap[update.workspaceId] + if (info == null) { + try { + info = client.syncWorkspace(update.workspaceId) + } catch (t: Throwable) { + thisLogger().error("${gitpodHost}: ${update.workspaceId}: failed to sync", t) + continue + } + workspacesMap[update.workspaceId] = info + } else if (WorkspaceInstance.isUpToDate(info.latestInstance, update)) { + continue + } else { + info.latestInstance = update + } + updateView() + } + } + updateLifetime.onTerminationOrNow { job.cancel() } + } + } +} \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodSettingsConfigurable.kt b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodSettingsConfigurable.kt new file mode 100644 index 00000000000000..ce6cd89cd925bf --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodSettingsConfigurable.kt @@ -0,0 +1,46 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.gateway + +import com.intellij.openapi.components.service +import com.intellij.openapi.options.BoundConfigurable +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.LabelPosition +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.gridLayout.HorizontalAlign +import com.intellij.ui.layout.ValidationInfoBuilder + +class GitpodSettingsConfigurable : BoundConfigurable("Gitpod") { + + override fun createPanel(): DialogPanel { + val state = service() + return panel { + row { + textField() + .label("Gitpod Host:", LabelPosition.LEFT) + .horizontalAlign(HorizontalAlign.FILL) + .bindText(state::gitpodHost) + .validationOnApply(::validateGitpodHost) + .validationOnInput(::validateGitpodHost) + } + } + } + + private fun validateGitpodHost( + builder: ValidationInfoBuilder, + gitpodHost: JBTextField + ): ValidationInfo? { + return builder.run { + if (gitpodHost.text.isBlank()) { + return@run error("may not be empty") + } + return@run null + } + } + +} \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodSettingsState.kt b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodSettingsState.kt new file mode 100644 index 00000000000000..82e86d8801c035 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/gateway/GitpodSettingsState.kt @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.gateway + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.util.EventDispatcher +import com.intellij.util.xmlb.XmlSerializerUtil +import java.net.URL +import java.util.* + +@State( + name = "io.gitpod.jetbrains.gateway.GitpodSettingsState", + storages = [Storage("gitpod.xml")] +) +class GitpodSettingsState : PersistentStateComponent { + + var gitpodHost: String = "gitpod.io" + set(value) { + if (value.isNullOrBlank()) { + return; + } + val gitpodHost = try { + URL(value.trim()).host + } catch (t: Throwable) { + value.trim() + } + if (gitpodHost == field) { + return + } + field = gitpodHost + dispatcher.multicaster.didChange() + } + + private interface Listener : EventListener { + fun didChange() + } + + private val dispatcher = EventDispatcher.create(Listener::class.java) + fun addListener(listener: () -> Unit): Disposable { + val internalListener = object : Listener { + override fun didChange() { + listener() + } + } + dispatcher.addListener(internalListener); + return Disposable { dispatcher.removeListener(internalListener) } + } + + override fun getState(): GitpodSettingsState { + return this + } + + override fun loadState(state: GitpodSettingsState) { + XmlSerializerUtil.copyBean(state, this) + } + +} \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/icons/GitpodIcons.kt b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/icons/GitpodIcons.kt new file mode 100644 index 00000000000000..caff51450146f4 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/kotlin/io/gitpod/jetbrains/icons/GitpodIcons.kt @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.icons + +import com.intellij.openapi.util.IconLoader + +object GitpodIcons { + @JvmField + val Logo = IconLoader.getIcon("/icons/logo.svg", javaClass) + + @JvmField + val Logo2x = IconLoader.getIcon("/icons/logo2x.svg", javaClass) + + @JvmField + val Logo4x = IconLoader.getIcon("/icons/logo4x.svg", javaClass) + + @JvmField + val Starting = IconLoader.getIcon("/icons/starting.svg", javaClass) + + @JvmField + val Running = IconLoader.getIcon("/icons/running.svg", javaClass) + + @JvmField + val Failed = IconLoader.getIcon("/icons/failed.svg", javaClass) + + @JvmField + val Stopped = IconLoader.getIcon("/icons/stopped.svg", javaClass) +} \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/resources/META-INF/plugin.xml b/components/ide/jetbrains/gateway-plugin/src/main/resources/META-INF/plugin.xml new file mode 100644 index 00000000000000..071af41c8b0ac4 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,32 @@ + + + + io.gitpod.jetbrains.gateway + Gitpod Gateway + Gitpod + + + + com.intellij.modules.platform + com.jetbrains.gateway + + + + + + + + + + + + + + + diff --git a/components/ide/jetbrains/gateway-plugin/src/main/resources/META-INF/pluginIcon.svg b/components/ide/jetbrains/gateway-plugin/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 00000000000000..684d39aaba3f23 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/failed.svg b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/failed.svg new file mode 100644 index 00000000000000..6700e6a44146c0 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/failed.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/logo.svg b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/logo.svg new file mode 100644 index 00000000000000..55d689fa09d504 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/logo2x.svg b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/logo2x.svg new file mode 100644 index 00000000000000..075adeb6fa079c --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/logo2x.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/logo4x.svg b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/logo4x.svg new file mode 100644 index 00000000000000..25106453971927 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/logo4x.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/running.svg b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/running.svg new file mode 100644 index 00000000000000..31ecc1a8caf0bf --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/running.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/starting.svg b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/starting.svg new file mode 100644 index 00000000000000..a74e600ce2b623 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/starting.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/stopped.svg b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/stopped.svg new file mode 100644 index 00000000000000..3cf0862f09b495 --- /dev/null +++ b/components/ide/jetbrains/gateway-plugin/src/main/resources/icons/stopped.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/components/ide/jetbrains/image/BUILD.yaml b/components/ide/jetbrains/image/BUILD.yaml index 10b93bec0f4900..01d70350dbd091 100644 --- a/components/ide/jetbrains/image/BUILD.yaml +++ b/components/ide/jetbrains/image/BUILD.yaml @@ -13,10 +13,9 @@ packages: srcs: - "startup.sh" - "supervisor-ide-config_intellij.json" - - "status/go.mod" - - "status/main.go" deps: - components/ide/jetbrains/backend-plugin:plugin + - components/ide/jetbrains/image/status:app argdeps: - imageRepoBase - intellijDownloadUrl @@ -35,10 +34,9 @@ packages: srcs: - "startup.sh" - "supervisor-ide-config_goland.json" - - "status/go.mod" - - "status/main.go" deps: - components/ide/jetbrains/backend-plugin:plugin + - components/ide/jetbrains/image/status:app argdeps: - imageRepoBase - golandDownloadUrl @@ -57,10 +55,9 @@ packages: srcs: - "startup.sh" - "supervisor-ide-config_pycharm.json" - - "status/go.mod" - - "status/main.go" deps: - components/ide/jetbrains/backend-plugin:plugin + - components/ide/jetbrains/image/status:app argdeps: - imageRepoBase - pycharmDownloadUrl @@ -79,10 +76,9 @@ packages: srcs: - "startup.sh" - "supervisor-ide-config_phpstorm.json" - - "status/go.mod" - - "status/main.go" deps: - components/ide/jetbrains/backend-plugin:plugin + - components/ide/jetbrains/image/status:app argdeps: - imageRepoBase - phpstormDownloadUrl diff --git a/components/ide/jetbrains/image/leeway.Dockerfile b/components/ide/jetbrains/image/leeway.Dockerfile index ee2393e8788d61..56c3cbbc453664 100644 --- a/components/ide/jetbrains/image/leeway.Dockerfile +++ b/components/ide/jetbrains/image/leeway.Dockerfile @@ -2,22 +2,17 @@ # Licensed under the GNU Affero General Public License (AGPL). # See License-AGPL.txt in the project root for license information. -FROM golang:1.17 AS build -WORKDIR /app -COPY status/* /app/ -RUN go build -o status - FROM alpine:3.15 as download ARG JETBRAINS_BACKEND_URL WORKDIR /workdir RUN apk add --no-cache --upgrade curl gzip tar unzip RUN curl -sSLo backend.tar.gz "$JETBRAINS_BACKEND_URL" && tar -xf backend.tar.gz --strip-components=1 && rm backend.tar.gz -COPY --chown=33333:33333 components-ide-jetbrains-backend-plugin--plugin/build/distributions/jetbrains-backend-plugin-1.0-SNAPSHOT.zip /workdir -RUN unzip jetbrains-backend-plugin-1.0-SNAPSHOT.zip -d plugins/ && rm jetbrains-backend-plugin-1.0-SNAPSHOT.zip +COPY --chown=33333:33333 components-ide-jetbrains-backend-plugin--plugin/build/distributions/gitpod-remote-0.0.1.zip /workdir +RUN unzip gitpod-remote-0.0.1.zip -d plugins/ && rm gitpod-remote-0.0.1.zip FROM scratch ARG SUPERVISOR_IDE_CONFIG COPY --chown=33333:33333 ${SUPERVISOR_IDE_CONFIG} /ide-desktop/supervisor-ide-config.json COPY --chown=33333:33333 startup.sh /ide-desktop/ -COPY --chown=33333:33333 --from=build /app/status /ide-desktop/ COPY --chown=33333:33333 --from=download /workdir/ /ide-desktop/backend/ +COPY --chown=33333:33333 components-ide-jetbrains-image-status--app/status /ide-desktop \ No newline at end of file diff --git a/components/ide/jetbrains/image/startup.sh b/components/ide/jetbrains/image/startup.sh index fb351877a4c647..5e84ac58076a26 100755 --- a/components/ide/jetbrains/image/startup.sh +++ b/components/ide/jetbrains/image/startup.sh @@ -16,9 +16,7 @@ until curl -sS "$SUPERVISOR_ADDR"/_supervisor/v1/status/content/wait/true | grep done echo "Desktop IDE: Content available." -export CWM_NON_INTERACTIVE=1 -export CWM_HOST_PASSWORD=gitpod export CWM_HOST_STATUS_OVER_HTTP_TOKEN=gitpod -/ide-desktop/backend/bin/remote-dev-server.sh cwmHost "$GITPOD_REPO_ROOT" > >(sed 's/^/JetBrains remote-dev-server.sh (out): /') 2> >(sed 's/^/JetBrains remote-dev-server.sh (err): /' >&2) +/ide-desktop/backend/bin/remote-dev-server.sh run "$GITPOD_REPO_ROOT" echo "Desktop IDE startup script exited" diff --git a/components/ide/jetbrains/image/status/BUILD.yaml b/components/ide/jetbrains/image/status/BUILD.yaml new file mode 100644 index 00000000000000..d55c21cf29bca6 --- /dev/null +++ b/components/ide/jetbrains/image/status/BUILD.yaml @@ -0,0 +1,14 @@ +packages: + - name: app + type: go + srcs: + - "**/*.go" + - "go.mod" + - "go.sum" + env: + - CGO_ENABLED=0 + - GOOS=linux + deps: + - components/supervisor-api/go:lib + config: + packaging: app diff --git a/components/ide/jetbrains/image/status/go.mod b/components/ide/jetbrains/image/status/go.mod index 9f8e692f627b16..5757150f3ff2a8 100644 --- a/components/ide/jetbrains/image/status/go.mod +++ b/components/ide/jetbrains/image/status/go.mod @@ -1,3 +1,19 @@ module github.com/gitpod-io/gitpod/jetbrains/status go 1.17 + +replace github.com/gitpod-io/gitpod/supervisor/api => ../../../../supervisor-api/go // leeway + +require github.com/gitpod-io/gitpod/supervisor/api v0.0.0-00010101000000-000000000000 + +require ( + github.com/golang/protobuf v1.5.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 // indirect + golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect + golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect + golang.org/x/text v0.3.5 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 + google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect + google.golang.org/grpc v1.39.1 // indirect + google.golang.org/protobuf v1.27.1 // indirect +) diff --git a/components/ide/jetbrains/image/status/go.sum b/components/ide/jetbrains/image/status/go.sum new file mode 100644 index 00000000000000..96df188489ee32 --- /dev/null +++ b/components/ide/jetbrains/image/status/go.sum @@ -0,0 +1,404 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 h1:ajue7SzQMywqRjg2fK7dcpc0QhFGpTR2plWfV4EZWR4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0/go.mod h1:r1hZAcvfFXuYmcKyCJI9wlyOPIZUJl6FCB8Cpca/NLE= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced h1:c5geK1iMU3cDKtFrCVQIcjR3W+JOZMuhIyICMCTbtus= +google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.1 h1:f37vZbBVTiJ6jKG5mWz8ySOBxNqy6ViPgyhSdVnxF3E= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/components/ide/jetbrains/image/status/main.go b/components/ide/jetbrains/image/status/main.go index bfb2cd2f8c8e7e..9f3cb3d05242f5 100644 --- a/components/ide/jetbrains/image/status/main.go +++ b/components/ide/jetbrains/image/status/main.go @@ -5,13 +5,19 @@ package main import ( + "context" "encoding/json" "fmt" "io/ioutil" "log" "net/http" + "net/url" "os" "time" + + supervisor "github.com/gitpod-io/gitpod/supervisor/api" + "golang.org/x/xerrors" + "google.golang.org/grpc" ) // proxy for the Code With Me status endpoints that transforms it into the supervisor status format. @@ -28,55 +34,38 @@ func main() { errlog := log.New(os.Stderr, "JetBrains IDE status: ", log.LstdFlags) - http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { - var ( - url = "http://localhost:63342/codeWithMe/unattendedHostStatus?token=gitpod" - client = http.Client{Timeout: 1 * time.Second} - ) - resp, err := client.Get(url) + http.HandleFunc("/joinLink", func(w http.ResponseWriter, r *http.Request) { + jsonLink, err := resolveJsonLink() if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } - defer resp.Body.Close() - - bodyBytes, err := ioutil.ReadAll(resp.Body) + fmt.Fprint(w, jsonLink) + }) + http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { + wsInfo, err := resolveWorkspaceInfo(context.Background()) if err != nil { - errlog.Printf("Error reading body: %v\n", err) + errlog.Printf("cannot get workspace info: %v\n", err) http.Error(w, err.Error(), http.StatusServiceUnavailable) return } - - if resp.StatusCode != http.StatusOK { - errlog.Printf("Getting non-200 status - %d\n%s\n", resp.StatusCode, bodyBytes) - http.Error(w, string(bodyBytes), resp.StatusCode) - return - } - - type Projects struct { - JoinLink string `json:"joinLink"` - } - type Response struct { - Projects []Projects `json:"projects"` - } - jsonResp := &Response{} - err = json.Unmarshal(bodyBytes, &jsonResp) - + gitpodUrl, err := url.Parse(wsInfo.GitpodHost) if err != nil { - errlog.Printf("Error parsing JSON body from IDE status probe: %v\n", err) - http.Error(w, "Error parsing JSON body from IDE status probe.", http.StatusServiceUnavailable) + errlog.Printf("cannot parse gitpod url: %v\n", err) + http.Error(w, err.Error(), http.StatusServiceUnavailable) return } - if len(jsonResp.Projects) != 1 { - errlog.Printf("projects size != 1\n") - http.Error(w, "projects size != 1", http.StatusServiceUnavailable) - return + link := url.URL{ + Scheme: "jetbrains-gateway", + Host: "connect", + RawQuery: fmt.Sprintf("gitpodHost=%s&workspaceId=%s", url.QueryEscape(gitpodUrl.Hostname()), url.QueryEscape(wsInfo.WorkspaceId)), } response := make(map[string]string) - response["link"] = jsonResp.Projects[0].JoinLink + response["link"] = link.String() response["label"] = label + response["clientID"] = "jetbrains-gateway" w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) }) fmt.Printf("Starting status proxy for desktop IDE at port %s\n", port) @@ -84,3 +73,55 @@ func main() { log.Fatal(err) } } + +type Projects struct { + JoinLink string `json:"joinLink"` +} +type Response struct { + Projects []Projects `json:"projects"` +} + +func resolveJsonLink() (string, error) { + var ( + hostStatusUrl = "http://localhost:63342/codeWithMe/unattendedHostStatus?token=gitpod" + client = http.Client{Timeout: 1 * time.Second} + ) + resp, err := client.Get(hostStatusUrl) + if err != nil { + return "", err + } + defer resp.Body.Close() + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + if resp.StatusCode != http.StatusOK { + return "", xerrors.Errorf("failed to resolve project status: %s (%d)", bodyBytes, resp.StatusCode) + } + jsonResp := &Response{} + err = json.Unmarshal(bodyBytes, &jsonResp) + if err != nil { + return "", err + } + if len(jsonResp.Projects) != 1 { + return "", xerrors.Errorf("project is not found") + } + return jsonResp.Projects[0].JoinLink, nil +} + +func resolveWorkspaceInfo(ctx context.Context) (*supervisor.WorkspaceInfoResponse, error) { + supervisorAddr := os.Getenv("SUPERVISOR_ADDR") + if supervisorAddr == "" { + supervisorAddr = "localhost:22999" + } + supervisorConn, err := grpc.Dial(supervisorAddr, grpc.WithInsecure()) + if err != nil { + return nil, xerrors.Errorf("failed connecting to supervisor: %w", err) + } + defer supervisorConn.Close() + wsinfo, err := supervisor.NewInfoServiceClient(supervisorConn).WorkspaceInfo(ctx, &supervisor.WorkspaceInfoRequest{}) + if err != nil { + return nil, xerrors.Errorf("failed getting workspace info from supervisor: %w", err) + } + return wsinfo, nil +} diff --git a/components/server/src/websocket/websocket-connection-manager.ts b/components/server/src/websocket/websocket-connection-manager.ts index e2acd595e763f2..d697aeeec648de 100644 --- a/components/server/src/websocket/websocket-connection-manager.ts +++ b/components/server/src/websocket/websocket-connection-manager.ts @@ -31,7 +31,7 @@ const EVENT_CLIENT_CONTEXT_CREATED = "EVENT_CLIENT_CONTEXT_CREATED"; const EVENT_CLIENT_CONTEXT_CLOSED = "EVENT_CLIENT_CONTEXT_CLOSED"; /** TODO(gpl) Refine this list */ -export type WebsocketClientType = "browser" | "go-client" | "gitpod-code" | "supervisor" | "local-companion"; +export type WebsocketClientType = "browser" | "go-client" | "gitpod-code" | "supervisor" | "local-companion" | "io.gitpod.jetbrains.remote" | "io.gitpod.jetbrains.gateway"; namespace WebsocketClientType { export function getClientType(req: express.Request): WebsocketClientType | undefined { const userAgent = req.headers["user-agent"]; @@ -48,6 +48,8 @@ namespace WebsocketClientType { result = "supervisor"; } else if (userAgent.startsWith("gitpod/local-companion")) { result = "local-companion"; + } else if(userAgent === 'io.gitpod.jetbrains.remote' || userAgent === 'io.gitpod.jetbrains.gateway') { + result = userAgent; } } if (result === undefined) { diff --git a/components/supervisor-api/java/build.gradle b/components/supervisor-api/java/build.gradle index a220f59737c3a4..97faf3f01b5211 100644 --- a/components/supervisor-api/java/build.gradle +++ b/components/supervisor-api/java/build.gradle @@ -32,6 +32,11 @@ java { withJavadocJar() } +compileJava { + sourceCompatibility = "11" + targetCompatibility = "11" +} + publishing { publications { mavenJava(MavenPublication) { diff --git a/components/supervisor/frontend/src/ide/heart-beat.ts b/components/supervisor/frontend/src/ide/heart-beat.ts index ef5ff8519b90f4..3471ad010c0a70 100644 --- a/components/supervisor/frontend/src/ide/heart-beat.ts +++ b/components/supervisor/frontend/src/ide/heart-beat.ts @@ -4,6 +4,8 @@ * See License-AGPL.txt in the project root for license information. */ +import { DisposableCollection, Disposable } from '@gitpod/gitpod-protocol/lib/util/disposable'; + let lastActivity = 0; const updateLastActivitiy = () => { lastActivity = new Date().getTime(); @@ -13,11 +15,12 @@ export const track = (w: Window) => { w.document.addEventListener('keydown', updateLastActivitiy, { capture: true }); } -let intervalHandle: NodeJS.Timer | undefined; +let toCancel: DisposableCollection | undefined; export function schedule(instanceId: string): void { - if (intervalHandle !== undefined) { + if (toCancel) { return; } + toCancel = new DisposableCollection() const sendHeartBeat = async (wasClosed?: true) => { try { await window.gitpod.service.server.sendHeartBeat({ instanceId, wasClosed }); @@ -26,12 +29,26 @@ export function schedule(instanceId: string): void { } } sendHeartBeat(); - window.addEventListener('beforeunload', () => { + let unloadTimeout: any; + const beforeUnloadListener = () => { + unloadTimeout = setTimeout(() => { + // if unload was cancelled then resume heartbeating + sendHeartBeat(); + }, 2000); sendHeartBeat(true); - }, { once: true }); + }; + const unloadListener = () => { + if (unloadTimeout) { + clearTimeout(unloadTimeout); + } + } + window.addEventListener('beforeunload', beforeUnloadListener); + window.addEventListener('unload', unloadListener); + toCancel.push(Disposable.create(() => window.removeEventListener('beforeunload', beforeUnloadListener))); + toCancel.push(Disposable.create(() => window.removeEventListener('unload', unloadListener))); let activityInterval = 30000; - intervalHandle = setInterval(() => { + const intervalHandle = setInterval(() => { // add an additional random value between 5 and 15 seconds const randomInterval = Math.floor(Math.random() * (15000 - 5000 + 1)) + 5000; if (lastActivity + activityInterval + randomInterval < new Date().getTime()) { @@ -40,11 +57,12 @@ export function schedule(instanceId: string): void { } sendHeartBeat(); }, activityInterval); + toCancel.push(Disposable.create(() => clearInterval(intervalHandle))); } export const cancel = () => { - if (intervalHandle !== undefined) { - clearInterval(intervalHandle); - intervalHandle = undefined; + if (toCancel) { + toCancel.dispose(); + toCancel = undefined; } } \ No newline at end of file diff --git a/installer/pkg/components/server/ide/configmap.go b/installer/pkg/components/server/ide/configmap.go index fae96277b7b86b..95d3d602c31dec 100644 --- a/installer/pkg/components/server/ide/configmap.go +++ b/installer/pkg/components/server/ide/configmap.go @@ -53,7 +53,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { DefaultDesktopIDE: intellij, DesktopIDEs: []string{intellij, goland, pycharm, phpstorm}, InstallationSteps: []string{ - "If you don't see an open dialog by the browser, make sure you have JetBrains Gateway with Gitpod Plugin installed on your machine, and then click ${OPEN_LINK_LABEL} below.", + "If you don't see an open dialog by the browser, make sure you have JetBrains Gateway with Gitpod Plugin installed on your machine, and then click ${OPEN_LINK_LABEL} below.", }, }, }, @@ -96,7 +96,6 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { Title: "IntelliJ IDEA", Type: typeDesktop, Logo: getIdeLogoPath("intellijIdeaLogo"), - Notes: []string{"While in beta, when you open a workspace with IntelliJ IDEA you will need to use the password “gitpod”."}, Image: common.ImageName(ctx.Config.Repository, ide.IntelliJDesktopIDEImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.IntelliJImage.Version), }, goland: { @@ -104,7 +103,6 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { Title: "GoLand", Type: typeDesktop, Logo: getIdeLogoPath("golandLogo"), - Notes: []string{"While in beta, when you open a workspace with GoLand you will need to use the password “gitpod”."}, Image: common.ImageName(ctx.Config.Repository, ide.GoLandDesktopIdeImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.GoLandImage.Version), }, pycharm: { @@ -112,7 +110,6 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { Title: "PyCharm", Type: typeDesktop, Logo: getIdeLogoPath("pycharmLogo"), - Notes: []string{"While in beta, when you open a workspace with PyCharm you will need to use the password “gitpod”."}, Image: common.ImageName(ctx.Config.Repository, ide.PyCharmDesktopIdeImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.PyCharmImage.Version), }, phpstorm: { @@ -120,7 +117,6 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { Title: "PhpStorm", Type: typeDesktop, Logo: getIdeLogoPath("phpstormLogo"), - Notes: []string{"While in beta, when you open a workspace with PhpStorm you will need to use the password “gitpod”."}, Image: common.ImageName(ctx.Config.Repository, ide.PhpStormDesktopIdeImage, ctx.VersionManifest.Components.Workspace.DesktopIdeImages.PhpStormImage.Version), }, },