/
GitpodCLIService.kt
147 lines (132 loc) · 5.59 KB
/
GitpodCLIService.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// 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
import com.intellij.codeWithMe.ClientId
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.client.ClientSession
import com.intellij.openapi.client.ClientSessionsManager
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.util.application
import com.intellij.util.withFragment
import com.intellij.util.withPath
import com.intellij.util.withQuery
import com.jetbrains.rd.util.URI
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 io.prometheus.client.exporter.common.TextFormat
import kotlinx.coroutines.*
import org.jetbrains.ide.RestService
import org.jetbrains.io.response
import java.io.OutputStreamWriter
import java.nio.file.InvalidPathException
import java.nio.file.Path
@Suppress("UnstableApiUsage", "OPT_IN_USAGE")
class GitpodCLIService : RestService() {
private val manager = service<GitpodManager>()
private val portsService = service<GitpodPortsService>()
private val cliHelperService = service<GitpodCLIHelper>()
override fun getServiceName() = SERVICE_NAME
override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
val operation = getStringParameter("op", urlDecoder)
if (application.isHeadlessEnvironment) {
return "not supported in headless mode"
}
/**
* prod: curl http://localhost:63342/api/gitpod/cli?op=metrics
* dev: curl http://localhost:63343/api/gitpod/cli?op=metrics
*/
if (operation == "metrics") {
val out = BufferExposingByteArrayOutputStream()
val writer = OutputStreamWriter(out)
TextFormat.write004(writer, manager.registry.metricFamilySamples())
writer.close()
val response = response(TextFormat.CONTENT_TYPE_004, Unpooled.wrappedBuffer(out.internalBuffer, 0, out.size()))
sendResponse(request, context, response)
return null
}
if (operation == "open") {
val fileStr = getStringParameter("file", urlDecoder)
if (fileStr.isNullOrBlank()) {
return "file is missing"
}
val file = parseFilePath(fileStr) ?: return "invalid file"
val shouldWait = getBooleanParameter("wait", urlDecoder)
return withClient(request, context) {
cliHelperService.open(file, shouldWait)
}
}
if (operation == "preview") {
val url = getStringParameter("url", urlDecoder)
if (url.isNullOrBlank()) {
return "url is missing"
}
val resolvedUrl = resolveExternalUrl(url)
return withClient(request, context) { project ->
BrowserUtil.browse(resolvedUrl, project)
}
}
return "invalid operation"
}
private fun resolveExternalUrl(url: String): String {
val uri = URI.create(url)
val optionalLocalHostUriMetadata = portsService.extractLocalHostUriMetaDataForPortMapping(uri)
return when {
optionalLocalHostUriMetadata.isEmpty -> url
else -> portsService.getLocalHostUriFromHostPort(optionalLocalHostUriMetadata.get().port)
.withPath(uri.path)
.withQuery(uri.query)
.withFragment(uri.fragment)
.toString()
}
}
private fun withClient(request: FullHttpRequest, context: ChannelHandlerContext, action: suspend (project: Project?) -> Unit): String? {
GlobalScope.launch {
getClientSessionAndProjectAsync().let { (session, project) ->
ClientId.withClientId(session.clientId) {
action(project)
sendOk(request, context)
}
}
}
return null
}
private data class ClientSessionAndProject(val session: ClientSession, val project: Project?)
private suspend fun getClientSessionAndProjectAsync(): ClientSessionAndProject {
val project = getLastFocusedOrOpenedProject()
var session: ClientSession? = null
while (session == null) {
if (project != null) {
session = ClientSessionsManager.getProjectSessions(project, false).firstOrNull()
}
if (session == null) {
session = ClientSessionsManager.getAppSessions(false).firstOrNull()
}
if (session == null) {
delay(1000L)
}
}
return ClientSessionAndProject(session, project)
}
private fun parseFilePath(path: String): Path? {
return try {
var file: Path = Path.of(FileUtilRt.toSystemDependentName(path)) // handle paths like '/file/foo\qwe'
if (!file.isAbsolute) {
file = file.toAbsolutePath()
}
file.normalize()
} catch (e: InvalidPathException) {
thisLogger().warn("gitpod cli: failed to parse file path:", e)
null
}
}
companion object {
const val SERVICE_NAME = "gitpod/cli"
}
}