Browse Source

Improve handling of preview errors (#1502)

pull/1508/head
Alexey Tsvetkov 3 years ago committed by GitHub
parent
commit
9686eb2acd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/Command.kt
  2. 2
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/ExitCodes.kt
  3. 21
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewErrorReporter.kt
  4. 8
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewException.kt
  5. 60
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt
  6. 21
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt
  7. 22
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt
  8. 1
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/constants.kt
  9. 2
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/protocolVersion.kt
  10. 51
      gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/utils/RingBuffer.kt
  11. 60
      gradle-plugins/preview-rpc/src/test/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/utils/RingBufferTest.kt
  12. 27
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/IdePreviewErrorReporter.kt
  13. 2
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt
  14. 17
      idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt

1
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/Command.kt

@ -9,6 +9,7 @@ data class Command(val type: Type, val args: List<String>) {
enum class Type {
ATTACH,
FRAME,
ERROR,
PREVIEW_CONFIG,
PREVIEW_CLASSPATH,
PREVIEW_FQ_NAME,

2
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/ExitCodes.kt

@ -8,4 +8,6 @@ package org.jetbrains.compose.desktop.ui.tooling.preview.rpc
internal object ExitCodes {
const val OK = 0
const val COULD_NOT_CONNECT_TO_PREVIEW_MANAGER = 1
const val RECEIVER_FATAL_ERROR = 2
const val SENDER_FATAL_ERROR = 3
}

21
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewErrorReporter.kt

@ -0,0 +1,21 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.desktop.ui.tooling.preview.rpc
interface PreviewErrorReporter {
fun report(e: Throwable, details: String? = null)
fun report(e: String, details: String? = null)
}
object StderrPreviewErrorReporter : PreviewErrorReporter {
override fun report(e: Throwable, details: String?) {
report(e.stackTraceString)
}
override fun report(e: String, details: String?) {
System.err.println(e)
}
}

8
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewException.kt

@ -0,0 +1,8 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.desktop.ui.tooling.preview.rpc
class PreviewException(message: String) : RuntimeException(message)

60
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/PreviewManager.kt

@ -5,6 +5,7 @@
package org.jetbrains.compose.desktop.ui.tooling.preview.rpc
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.utils.RingBuffer
import java.io.IOException
import java.net.ServerSocket
import java.net.SocketTimeoutException
@ -49,8 +50,10 @@ private data class RunningPreview(
}
class PreviewManagerImpl(
private val previewListener: PreviewListener = PreviewListenerBase()
private val previewListener: PreviewListener = PreviewListenerBase(),
private val errorReporter: PreviewErrorReporter = StderrPreviewErrorReporter
) : PreviewManager {
// todo: add quiet mode
private val log = PrintStreamLogger("SERVER")
private val previewSocket = newServerSocket()
private val gradleCallbackSocket = newServerSocket()
@ -78,9 +81,9 @@ class PreviewManagerImpl(
PREVIEW_HOST_CLASS_NAME,
previewSocket.localPort.toString()
).apply {
// todo: non verbose mode
redirectOutput(ProcessBuilder.Redirect.INHERIT)
redirectError(ProcessBuilder.Redirect.INHERIT)
redirectOutput(ProcessBuilder.Redirect.PIPE)
redirectError(ProcessBuilder.Redirect.PIPE)
redirectErrorStream(true)
}.start()
val runningPreview = runningPreview.get()
@ -91,6 +94,39 @@ class PreviewManagerImpl(
connection?.receiveAttach(listener = previewListener) {
this.runningPreview.set(RunningPreview(connection, process))
}
val processLogLines = RingBuffer<String>(512)
val exception = StringBuilder()
var exceptionMarker = false
process.inputStream.bufferedReader().forEachLine { line ->
if (exceptionMarker) {
exception.appendLine(line)
} else {
if (line.startsWith(PREVIEW_START_OF_STACKTRACE_MARKER)) {
exceptionMarker = true
} else {
processLogLines.add(line)
}
}
}
while (process.isAlive) {
process.waitFor(5, TimeUnit.SECONDS)
if (process.isAlive) {
process.destroyForcibly()
process.waitFor(5, TimeUnit.SECONDS)
}
}
if (process.isAlive) error("Preview process does not finish!")
val exitCode = process.exitValue()
if (exitCode != ExitCodes.OK) {
val errorMessage = buildString {
appendLine("Preview process exited unexpectedly: exitCode=$exitCode")
if (exceptionMarker) {
appendLine(exception)
}
}
errorReporter.report(PreviewException(errorMessage), details = processLogLines.joinToString("\n"))
}
}
}
@ -115,13 +151,21 @@ class PreviewManagerImpl(
private val receivePreviewResponseThread = repeatWhileAliveThread("receivePreviewResponse") {
withLivePreviewConnection {
receiveFrame { renderedFrame ->
receiveFrame(
onFrame = { renderedFrame ->
inProcessRequest.get()?.let { request ->
processedRequest.set(request)
inProcessRequest.compareAndSet(request, null)
}
previewListener.onRenderedFrame(renderedFrame)
},
onError = { error ->
errorReporter.report(PreviewException(error))
previewHostConfig.set(null)
previewClasspath.set(null)
inProcessRequest.set(null)
}
)
}
}
@ -244,12 +288,12 @@ class PreviewManagerImpl(
Thread.sleep(sleepDelayMs)
} catch (e: InterruptedException) {
continue
} catch (e: Throwable) {
e.printStackTrace(System.err)
break
}
}
}.also {
it.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { thread, e ->
errorReporter.report(e)
}
threads.add(it)
it.start()
}

21
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt

@ -69,12 +69,17 @@ internal class PreviewHost(private val log: PreviewLogger, connection: RemoteCon
Thread.sleep(DEFAULT_SLEEP_DELAY_MS)
} catch (e: InterruptedException) {
continue
} catch (e: Exception) {
if (connection.isAlive) {
connection.sendError(e)
} else {
throw IllegalStateException("Could not report an exception: IDE connection is not alive", e)
}
}
}
}.setUpUnhandledExceptionHandler(ExitCodes.SENDER_FATAL_ERROR)
val receiverThread = thread {
try {
while (connection.isAlive) {
try {
connection.receivePreviewRequest(
@ -89,11 +94,21 @@ internal class PreviewHost(private val log: PreviewLogger, connection: RemoteCon
)
} catch (e: SocketTimeoutException) {
continue
} catch (e: InterruptedException) {
continue
}
}
} catch (e: Throwable) {
}.setUpUnhandledExceptionHandler(ExitCodes.RECEIVER_FATAL_ERROR)
private fun Thread.setUpUnhandledExceptionHandler(exitCode: Int): Thread = apply {
uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
try {
System.err.println()
System.err.println(PREVIEW_START_OF_STACKTRACE_MARKER)
e.printStackTrace(System.err)
exitProcess(1)
} finally {
exitProcess(exitCode)
}
}
}

22
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt

@ -32,14 +32,30 @@ internal fun RemoteConnection.sendFrame(frame: RenderedFrame) {
sendData(frame.bytes)
}
internal fun RemoteConnection.receiveFrame(fn: (RenderedFrame) -> Unit) {
internal fun RemoteConnection.sendError(e: Exception) {
sendCommand(Command.Type.ERROR)
sendUtf8StringData(e.stackTraceString)
}
internal fun RemoteConnection.receiveFrame(
onFrame: (RenderedFrame) -> Unit,
onError: (String) -> Unit
) {
receiveCommand { (type, args) ->
if (type == Command.Type.FRAME) {
when (type) {
Command.Type.FRAME -> {
receiveData { bytes ->
val (w, h) = args
val frame = RenderedFrame(bytes, width = w.toInt(), height = h.toInt())
fn(frame)
onFrame(frame)
}
}
Command.Type.ERROR -> {
receiveUtf8StringData { stacktrace ->
onError(stacktrace)
}
}
else -> error("Received unexpected command type: $type")
}
}
}

1
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/constants.kt

@ -11,3 +11,4 @@ internal const val MAX_CMD_SIZE = 8 * 1024
// 100 Mb should be enough even for 8K screenshots
internal const val MAX_BINARY_SIZE = 100 * 1024 * 1024
internal const val MAX_BUF_SIZE = 8 * 1024
internal const val PREVIEW_START_OF_STACKTRACE_MARKER = "<!--START OF COMPOSE PREVIEW PROCESS FATAL EXCEPTION--!>"

2
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/protocolVersion.kt

@ -5,4 +5,4 @@
package org.jetbrains.compose.desktop.ui.tooling.preview.rpc
const val PROTOCOL_VERSION = 1
const val PROTOCOL_VERSION = 2

51
gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/utils/RingBuffer.kt

@ -0,0 +1,51 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.desktop.ui.tooling.preview.rpc.utils
internal class RingBuffer<T : Any>(internal val maxSize: Int) : Iterable<T> {
private var start = 0
private var size = 0
private val values = arrayOfNulls<Any?>(maxSize)
init {
check(maxSize > 0) { "Max size should be a positive number: $maxSize" }
}
fun add(element: T) {
if (size < maxSize) {
size++
} else {
start = (start + 1) % maxSize
}
values[(start + size - 1) % maxSize] = element
}
fun addAll(elements: Iterable<T>) {
elements.forEach { add(it) }
}
fun clear() {
start = 0
size = 0
for (i in values.indices) {
values[i] = null
}
}
override fun iterator(): Iterator<T> =
object : Iterator<T> {
private var i = 0
override fun hasNext(): Boolean = i < size
override fun next(): T {
if (!hasNext()) throw NoSuchElementException()
@Suppress("UNCHECKED_CAST")
return values[(start + i++) % maxSize] as T
}
}
}

60
gradle-plugins/preview-rpc/src/test/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/utils/RingBufferTest.kt

@ -0,0 +1,60 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.desktop.ui.tooling.preview.rpc.utils
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
internal class RingBufferTest {
private fun numbers(size: Int): IntArray = IntArray(size) { it }
private fun testBuffer() = RingBuffer<Int>(4)
@Test
fun empty() {
val it = testBuffer().iterator()
assertFalse(it.hasNext())
}
@Test
fun addSmall() {
val buf = testBuffer()
val expected = numbers(buf.maxSize / 2)
buf.addAll(expected.asIterable())
assertArrayEquals(expected, buf.toList().toIntArray())
}
@Test
fun addMedium() {
val buf = testBuffer()
val expected = numbers(buf.maxSize + buf.maxSize / 2)
buf.addAll(expected.asIterable())
checkAllEquals(expected.takeLast(buf.maxSize), buf.toList())
}
@Test
fun addBig() {
val buf = testBuffer()
val expected = numbers(buf.maxSize * 3)
buf.addAll(expected.asIterable())
checkAllEquals(expected.takeLast(buf.maxSize), buf.toList())
}
@Test
fun testClear() {
val buf = testBuffer()
val expected = numbers(buf.maxSize * 3)
buf.addAll(expected.asIterable())
buf.clear()
checkAllEquals(emptyList(), buf.toList())
}
private fun <T> checkAllEquals(expected: Collection<T>, actual: Collection<T>) {
val expectedString = expected.joinToString(", ", prefix = "[", postfix = "]")
val actualString = actual.joinToString(", ", prefix = "[", postfix = "]")
assertEquals(expectedString, actualString)
}
}

27
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/IdePreviewErrorReporter.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.desktop.ide.preview
import com.intellij.openapi.diagnostic.Logger
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewErrorReporter
internal class IdePreviewErrorReporter(
private val logger: Logger,
private val previewStateService: PreviewStateService
) : PreviewErrorReporter {
override fun report(e: Throwable, details: String?) {
report(e.stackTraceToString(), details)
}
override fun report(e: String, details: String?) {
if (details != null) {
logger.error(e, details)
} else {
logger.error(e)
}
previewStateService.clearPreviewOnError()
}
}

2
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewPanel.kt

@ -29,7 +29,7 @@ internal class PreviewPanel : JPanel() {
}
}
fun previewImage(image: BufferedImage, imageDimension: Dimension) {
fun previewImage(image: BufferedImage?, imageDimension: Dimension?) {
synchronized(this) {
this.image = image
this.imageDimension = imageDimension

17
idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt

@ -9,6 +9,7 @@ import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.externalSystem.model.task.*
import com.intellij.openapi.externalSystem.service.notification.ExternalSystemProgressNotificationManager
import com.intellij.openapi.module.Module
@ -24,12 +25,16 @@ import javax.swing.event.AncestorListener
@Service
class PreviewStateService(private val myProject: Project) : Disposable {
private val idePreviewLogger = Logger.getInstance("org.jetbrains.compose.desktop.ide.preview")
private val previewListener = CompositePreviewListener()
private val previewManager: PreviewManager = PreviewManagerImpl(previewListener)
private val errorReporter = IdePreviewErrorReporter(idePreviewLogger, this)
private val previewManager: PreviewManager = PreviewManagerImpl(previewListener, errorReporter)
val gradleCallbackPort: Int
get() = previewManager.gradleCallbackPort
private val configurePreviewTaskNameCache =
ConfigurePreviewTaskNameCache(ConfigurePreviewTaskNameProviderImpl())
private var previewPanel: PreviewPanel? = null
private var loadingPanel: JBLoadingPanel? = null
init {
val projectRefreshListener = ConfigurePreviewTaskNameCacheInvalidator(configurePreviewTaskNameCache)
@ -44,12 +49,17 @@ class PreviewStateService(private val myProject: Project) : Disposable {
override fun dispose() {
previewManager.close()
configurePreviewTaskNameCache.invalidate()
previewPanel = null
loadingPanel = null
}
internal fun registerPreviewPanels(
previewPanel: PreviewPanel,
loadingPanel: JBLoadingPanel
) {
this.previewPanel = previewPanel
this.loadingPanel = loadingPanel
val previewResizeListener = PreviewResizeListener(previewManager)
previewPanel.addAncestorListener(previewResizeListener)
Disposer.register(this) { previewPanel.removeAncestorListener(previewResizeListener) }
@ -73,6 +83,11 @@ class PreviewStateService(private val myProject: Project) : Disposable {
})
}
internal fun clearPreviewOnError() {
loadingPanel?.stopLoading()
previewPanel?.previewImage(null, null)
}
internal fun buildStarted() {
previewListener.onNewBuildRequest()
}

Loading…
Cancel
Save