Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[JetBrains] Show notification when port becomes available 🔔 #10107

Merged
merged 1 commit into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ public interface GitpodServer {

@JsonRequest
CompletableFuture<IDEOptions> getIDEOptions();

@JsonRequest
CompletableFuture<WorkspaceInstancePort> openPort(String workspaceId, WorkspaceInstancePort port);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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 PortVisibility {
PUBLIC("public"), PRIVATE("private");

private final String toString;

private PortVisibility(String toString) {
this.toString = toString;
}

public String toString() {
return toString;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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 WorkspaceInstancePort {
private Number port;
private String visibility;
private String url;

public void setPort(Number port) {
this.port = port;
}

public void setVisibility(String visibility) {
this.visibility = visibility;
}

public void setUrl(String url) {
this.url = url;
}

public Number getPort() { return this.port; }

public String getVisibility() { return this.visibility; }

public String getUrl() { return this.url; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,51 @@

package io.gitpod.jetbrains.remote

import com.intellij.codeWithMe.ClientId
import com.intellij.ide.BrowserUtil
import com.intellij.idea.StartupUtil
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.client.ClientProjectSession
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.fileTypes.LanguageFileType
import com.intellij.remoteDev.util.onTerminationOrNow
import com.intellij.util.application
import com.jetbrains.rd.util.lifetime.Lifetime
import io.gitpod.gitpodprotocol.api.entities.RemoteTrackMessage
import io.gitpod.gitpodprotocol.api.entities.WorkspaceInstancePort
import io.gitpod.supervisor.api.Info
import kotlinx.coroutines.GlobalScope
import io.gitpod.supervisor.api.Status
import io.gitpod.supervisor.api.Status.PortVisibility
import io.gitpod.supervisor.api.Status.PortsStatus
import io.gitpod.supervisor.api.StatusServiceGrpc
import io.grpc.stub.ClientCallStreamObserver
import io.grpc.stub.ClientResponseObserver
import kotlinx.coroutines.*
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.jetbrains.ide.BuiltInServerManager
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture

class GitpodClientProjectSessionTracker(
private val session: ClientProjectSession
) {
) : Disposable {

private val manager = service<GitpodManager>()

private lateinit var info: Info.WorkspaceInfoResponse
private val versionName = ApplicationInfo.getInstance().versionName
private val fullVersion = ApplicationInfo.getInstance().fullVersion
private val lifetime = Lifetime.Eternal.createNested()

override fun dispose() {
lifetime.terminate()
}

init {
GlobalScope.launch {
Expand All @@ -35,6 +58,140 @@ class GitpodClientProjectSessionTracker(
}
}

private fun isExposedServedPort(port: Status.PortsStatus?) : Boolean {
if (port === null) {
return false
}
return port.served && port.hasExposed()
}

private fun showOpenServiceNotification(port: PortsStatus, offerMakePublic: Boolean = false) {
val message = "A service is available on port ${port.localPort}"
val notification = manager.notificationGroup.createNotification(message, NotificationType.INFORMATION)

val openBrowserAction = NotificationAction.createSimple("Open Browser") {
openBrowser(port.exposed.url)
}
notification.addAction(openBrowserAction)

if (offerMakePublic) {
val makePublicLambda = {
runBlocking {
makePortPublic(info.workspaceId, port)
}
}
val makePublicAction = NotificationAction.createSimple("Make Public", makePublicLambda)
notification.addAction(makePublicAction)
}

ClientId.withClientId(session.clientId) {
notification.notify(null)
}
}

private suspend fun makePortPublic(workspaceId: String, port: PortsStatus) {
val p = WorkspaceInstancePort()
p.port = port.localPort
p.visibility = io.gitpod.gitpodprotocol.api.entities.PortVisibility.PUBLIC.toString()
p.url = port.exposed.url

try {
manager.client.server.openPort(workspaceId, p).await()
Copy link
Contributor Author

@andreafalzetti andreafalzetti May 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at the moment, pressing the button doesn't update the UI (in VS Code in the ports view you can see the text "private" change into "public"), so it looks a bit unfinished. We could consider adding an extra notification like:

val message = "The port ${port.localPort} is now public"
val notification = manager.notificationGroup.createNotification(message, NotificationType.INFORMATION)
ClientId.withClientId(session.clientId) {
   val project = RestService.getLastFocusedOrOpenedProject()
   notification.notify(project)
}

Thoughts?

cc @akosyakov @loujaybee

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many notification is also not nice. Is it possible to update an action? Maybe just make it expirable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was a good idea, unfortunately if I do "Make Public" createSimpleExpiring - when I click it, it expires all actions including "Open Browser" :(

} catch (e: Exception) {
thisLogger().error("gitpod: failed to open port ${port.localPort}: ", e)
}
}

private fun openBrowser(url: String) {
ClientId.withClientId(session.clientId) {
BrowserUtil.browse(url)
}
}

private val portsObserveJob = GlobalScope.launch {
if (application.isHeadlessEnvironment) {
return@launch
}

// Ignore ports that aren't actually used by the user (e.g. ports used internally by JetBrains IDEs)
val backendPort = BuiltInServerManager.getInstance().waitForStart().port
val serverPort = StartupUtil.getServerFuture().await().port
val ignorePorts = listOf(backendPort, serverPort, 5990)
val portsStatus = hashMapOf<Int, Status.PortsStatus>()

val status = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel)
while (isActive) {
try {
val f = CompletableFuture<Void>()
status.portsStatus(
Status.PortsStatusRequest.newBuilder().setObserve(true).build(),
object : ClientResponseObserver<Status.PortsStatusRequest, Status.PortsStatusResponse> {

override fun beforeStart(requestStream: ClientCallStreamObserver<Status.PortsStatusRequest>) {
lifetime.onTerminationOrNow {
requestStream.cancel(null, null)
}
}

override fun onNext(ps: Status.PortsStatusResponse) {
for (port in ps.portsList) {
// Avoiding undesired notifications
if (ignorePorts.contains(port.localPort)) {
continue
}

val previous = portsStatus[port.localPort]
portsStatus[port.localPort] = port

val shouldSendNotification = !isExposedServedPort(previous) && isExposedServedPort(port)

if (shouldSendNotification) {
if (port.exposed.onExposed.number == Status.OnPortExposedAction.ignore_VALUE) {
continue
}

if (port.exposed.onExposed.number == Status.OnPortExposedAction.open_browser_VALUE || port.exposed.onExposed.number == Status.OnPortExposedAction.open_preview_VALUE) {
openBrowser(port.exposed.url)
continue
}

if (port.exposed.onExposed.number == Status.OnPortExposedAction.notify_VALUE) {
showOpenServiceNotification(port)
continue
}

if (port.exposed.onExposed.number == Status.OnPortExposedAction.notify_private_VALUE) {
showOpenServiceNotification(port, port.exposed.visibilityValue !== PortVisibility.public_visibility_VALUE)
continue
}
}
}
}

override fun onError(t: Throwable) {
f.completeExceptionally(t)
}

override fun onCompleted() {
f.complete(null)
}
})
f.await()
} catch (t: Throwable) {
if (t is CancellationException) {
throw t
}
thisLogger().error("gitpod: failed to stream ports status: ", t)
}
delay(1000L)
}
}
init {
lifetime.onTerminationOrNow {
portsObserveJob.cancel()
}
}

private fun registerActiveLanguageAnalytics() {
val activeLanguages = mutableSetOf<String>()
session.project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class GitpodManager : Disposable {
GitVcsApplicationSettings.getInstance().isUseCredentialHelper = true
}

private val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Gitpod Notifications")
val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Gitpod Notifications")
private val notificationsJob = GlobalScope.launch {
if (application.isHeadlessEnvironment) {
return@launch
Expand Down Expand Up @@ -234,6 +234,7 @@ class GitpodManager : Disposable {
.setHost(info.gitpodApi.host)
.addScope("function:sendHeartBeat")
.addScope("function:trackEvent")
.addScope("function:openPort")
.setKind("gitpod")
.build()

Expand Down