Commit 3b379e75 authored by Roman Shevchenko's avatar Roman Shevchenko Committed by intellij-monorepo-bot
Browse files

[platform] getting rid of VFS refresh in `OpenFileHttpService`

GitOrigin-RevId: a3e708cccdee7114a811f6cd97f2ef7c9fd236d7
parent ab74ca94
Branches unavailable Tags unavailable
No related merge requests found
Showing with 67 additions and 153 deletions
+67 -153
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.ide
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
......@@ -47,10 +47,11 @@ abstract class BuiltInServerTestCase {
protected open val urlPathPrefix = ""
protected fun doTest(urlSuffix: String? = null,
asSignedRequest: Boolean = true, origin: String? = null,
asSignedRequest: Boolean = true,
origin: String? = null,
responseStatus: Int = 200,
additionalCheck: ((connection: HttpResponse<InputStream>) -> Unit)? = null) {
val serviceUrl = "http://localhost:${BuiltInServerManager.getInstance().port}$urlPathPrefix"
val serviceUrl = "http://localhost:${BuiltInServerManager.getInstance().port}${urlPathPrefix}"
var url = serviceUrl
if (urlSuffix != null) {
url += urlSuffix
......@@ -61,11 +62,11 @@ abstract class BuiltInServerTestCase {
val line = manager.annotation?.line ?: -1
if (line != -1) {
url += ":$line"
url += ":${line}"
}
val column = manager.annotation?.column ?: -1
if (column != -1) {
url += ":$column"
url += ":${column}"
}
val expectedStatus = HttpResponseStatus.valueOf(manager.annotation?.status ?: responseStatus)
......@@ -102,4 +103,4 @@ internal fun testUrl(url: String, expectedStatus: HttpResponseStatus, asSignedRe
val response = client.send(builder.build(), HttpResponse.BodyHandlers.ofInputStream())
assertThat(HttpResponseStatus.valueOf(response.statusCode())).isEqualTo(expectedStatus)
return response
}
\ No newline at end of file
}
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.ide
import com.intellij.codeWithMe.ClientId
import com.intellij.ide.impl.ProjectUtil.focusProjectWindow
import com.intellij.ide.impl.ProjectUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
......@@ -14,26 +13,18 @@ import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtilRt
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.ManagingFS
import com.intellij.openapi.vfs.newvfs.RefreshQueue
import com.intellij.ui.AppUIUtil
import com.intellij.util.PathUtilRt
import com.intellij.util.io.systemIndependentPath
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.http.*
import org.jetbrains.builtInWebServer.WebServerPathToFileManager
import org.jetbrains.builtInWebServer.checkAccess
import org.jetbrains.concurrency.*
import org.jetbrains.io.send
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.regex.Pattern
import javax.swing.SwingUtilities
import kotlin.io.path.exists
import kotlin.math.max
private val NOT_FOUND = createError("not found")
private val LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$")
/**
......@@ -59,9 +50,6 @@ private val LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$")
* curl http://localhost:63342/api/file?file=path/to/file.kt&line=100&column=34
*/
internal class OpenFileHttpService : RestService() {
@Volatile private var refreshSessionId: Long = 0
private val requests = ConcurrentLinkedQueue<OpenFileTask>()
override fun getServiceName() = "file"
override fun isMethodSupported(method: HttpMethod) = method === HttpMethod.GET || method === HttpMethod.POST
......@@ -108,156 +96,81 @@ internal class OpenFileHttpService : RestService() {
return "UNC paths are not supported"
}
val promise = openFile(apiRequest, context, request) ?: return null
promise
.onSuccess {
sendOk(request, context)
}
.onError {
if (it === NOT_FOUND) {
// don't expose file status
sendStatus(HttpResponseStatus.NOT_FOUND.orInSafeMode(HttpResponseStatus.OK), keepAlive, channel)
LOG.warn("File ${apiRequest.file} not found")
}
else {
// todo send error
sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel)
LOG.error(it)
}
}
return null
}
private fun openFile(request: OpenFileRequest, context: ChannelHandlerContext, httpRequest: HttpRequest?): Promise<Void?>? {
val systemIndependentPath = FileUtil.toSystemIndependentName(FileUtil.expandUserHome(request.file!!))
val file = Paths.get(FileUtil.toSystemDependentName(systemIndependentPath))
if (file.isAbsolute) {
if (!file.exists()) {
return rejectedPromise(NOT_FOUND)
}
val vfsPath = FileUtil.toSystemIndependentName(FileUtil.expandUserHome(requestedFile))
val file = Path.of(FileUtil.toSystemDependentName(vfsPath))
val fileAndProject = if (!file.isAbsolute) {
findByRelativePath(FileUtil.toCanonicalPath(vfsPath, '/'))
}
else if (file.exists()) {
var isAllowed = checkAccess(file)
if (isAllowed && com.intellij.ide.impl.ProjectUtil.isRemotePath(systemIndependentPath)) {
// invokeAndWait is added to avoid processing many requests in this place: e.g. to prevent abuse of opening many remote files
if (isAllowed && ProjectUtil.isRemotePath(vfsPath)) {
// `invokeAndWait` is added to avoid processing many requests in this place: e.g., to prevent abuse of opening many remote files
SwingUtilities.invokeAndWait {
isAllowed = com.intellij.ide.impl.ProjectUtil.confirmLoadingFromRemotePath(systemIndependentPath, "warning.load.file.from.share", "title.load.file.from.share")
isAllowed = ProjectUtil.confirmLoadingFromRemotePath(vfsPath, "warning.load.file.from.share", "title.load.file.from.share")
}
}
if (isAllowed) {
return openAbsolutePath(file, request)
}
else {
HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.OK).send(context.channel(), httpRequest)
if (!isAllowed) {
HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.OK).send(context.channel(), request)
return null
}
findByAbsolutePath(file)
}
else null
// we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation
val queue = RefreshQueue.getInstance()
queue.cancelSession(refreshSessionId)
val mainTask = OpenFileTask(FileUtil.toCanonicalPath(systemIndependentPath, '/'), request)
requests.offer(mainTask)
val clientId = ClientId.ownerId
val session = queue.createSession(true, true, {
while (true) {
val task = requests.poll() ?: break
task.promise.catchError {
if (openRelativePath(task.path, task.request, clientId)) {
task.promise.setResult(null)
}
else {
task.promise.setError(NOT_FOUND)
}
}
}
}, ModalityState.nonModal())
session.addAllFiles(*ManagingFS.getInstance().localRoots)
refreshSessionId = session.id
session.launch()
return mainTask.promise
if (fileAndProject == null) {
// don't expose file status
sendStatus(HttpResponseStatus.NOT_FOUND.orInSafeMode(HttpResponseStatus.OK), keepAlive, channel)
LOG.warn("File ${requestedFile} not found")
}
else {
val (virtualFile, project) = fileAndProject
navigate(project, virtualFile, apiRequest)
sendOk(request, context)
}
return null
}
}
internal class OpenFileRequest {
var file: String? = null
// The line number of the file (1-based)
var line = 0
// The column number of the file (1-based)
var column = 0
var focused = true
}
private class OpenFileTask(val path: String, val request: OpenFileRequest) {
internal val promise = AsyncPromise<Void?>()
}
private fun navigate(project: Project?, file: VirtualFile, request: OpenFileRequest) {
val effectiveProject = project ?: RestService.getLastFocusedOrOpenedProject() ?: ProjectManager.getInstance().defaultProject
// OpenFileDescriptor line and column number are 0-based.
OpenFileDescriptor(effectiveProject, file, max(request.line - 1, 0), max(request.column - 1, 0)).navigate(true)
if (request.focused) {
focusProjectWindow(project, true)
internal class OpenFileRequest {
var file: String? = null
var line = 0 // The line number of the file (1-based)
var column = 0 // The column number of the file (1-based)
var focused = true
}
}
// path must be normalized
private fun openRelativePath(path: String, request: OpenFileRequest, clientId: ClientId): Boolean {
ClientId.withClientId(clientId) {
return openRelativePath(path, request)
private fun findByAbsolutePath(file: Path): Pair<VirtualFile, Project?>? {
val virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(file)
return if (virtualFile != null) virtualFile to runReadAction { guessProjectForContentFile(virtualFile) } else null
}
}
private fun openRelativePath(path: String, request: OpenFileRequest): Boolean {
var virtualFile: VirtualFile? = null
var project: Project? = null
val projects = ProjectManager.getInstance().openProjects
for (openedProject in projects) {
openedProject.baseDir?.let {
virtualFile = it.findFileByRelativePath(path)
}
if (virtualFile == null) {
virtualFile = WebServerPathToFileManager.getInstance(openedProject).findVirtualFile(path)
}
if (virtualFile != null) {
project = openedProject
break
private fun findByRelativePath(path: String): Pair<VirtualFile, Project?>? {
for (project in ProjectManager.getInstance().openProjects) {
@Suppress("DEPRECATION") val file =
project.baseDir?.findFileByRelativePath(path) ?: WebServerPathToFileManager.getInstance(project).findVirtualFile(path)
if (file != null) {
return file to project
}
}
return null
}
return virtualFile?.let {
AppUIUtil.invokeLaterIfProjectAlive(project!!, Runnable { navigate(project, it, request) })
true
} ?: false
}
private fun openAbsolutePath(file: Path, request: OpenFileRequest): Promise<Void?> {
val promise = AsyncPromise<Void?>()
val task = Runnable {
promise.catchError {
val virtualFile = runWriteAction {
LocalFileSystem.getInstance().refreshAndFindFileByPath(file.systemIndependentPath)
}
if (virtualFile == null) {
promise.setError(NOT_FOUND)
}
else {
navigate(guessProjectForContentFile(virtualFile), virtualFile, request)
promise.setResult(null)
private fun navigate(project: Project?, file: VirtualFile, request: OpenFileRequest) {
val clientId = ClientId.ownerId
val task = Runnable {
ClientId.withClientId(clientId) {
val effectiveProject = project ?: RestService.getLastFocusedOrOpenedProject() ?: ProjectManager.getInstance().defaultProject
// OpenFileDescriptor line and column number are 0-based.
OpenFileDescriptor(effectiveProject, file, max(request.line - 1, 0), max(request.column - 1, 0)).navigate(true)
if (request.focused) {
ProjectUtil.focusProjectWindow(project, true)
}
}
}
val app = ApplicationManager.getApplication()
if (app.isUnitTestMode) {
app.invokeAndWait(task)
}
else {
app.invokeLater(task)
}
}
val app = ApplicationManager.getApplication()
if (app.isUnitTestMode) {
app.invokeAndWait(task)
}
else {
app.invokeLater(task)
}
return promise
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment