fix(intellij): fix exception handling when initializing tabby. (#429)

release-0.2
Zhiming Ma 2023-09-11 21:23:54 +08:00 committed by GitHub
parent 5f6cbcaf94
commit 31217bcfc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 142 additions and 44 deletions

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
class Agent : ProcessAdapter() { class Agent : ProcessAdapter() {
@ -52,18 +54,18 @@ class Agent : ProcessAdapter() {
if (node?.exists() == true) { if (node?.exists() == true) {
logger.info("Node bin path: ${node.absolutePath}") logger.info("Node bin path: ${node.absolutePath}")
} else { } else {
logger.error("Node bin not found") throw AgentException("Node bin not found. Please install Node.js v16+ and add bin path to system environment variable PATH, then restart IDE.")
throw AgentException("Node bin not found")
} }
checkNodeVersion(node.absolutePath)
val script = val script =
PluginManagerCore.getPlugin(PluginId.getId("com.tabbyml.intellij-tabby"))?.pluginPath?.resolve("node_scripts/tabby-agent.js") PluginManagerCore.getPlugin(PluginId.getId("com.tabbyml.intellij-tabby"))?.pluginPath?.resolve("node_scripts/tabby-agent.js")
?.toFile() ?.toFile()
if (script?.exists() == true) { if (script?.exists() == true) {
logger.info("Node script path: ${script.absolutePath}") logger.info("Node script path: ${script.absolutePath}")
} else { } else {
logger.error("Node script not found") throw AgentException("Node script not found. Please reinstall Tabby plugin.")
throw AgentException("Node script not found")
} }
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath) val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
@ -77,6 +79,25 @@ class Agent : ProcessAdapter() {
streamWriter = process.processInput.writer() streamWriter = process.processInput.writer()
} }
private fun checkNodeVersion(node: String) {
try {
val process = GeneralCommandLine(node, "--version").createProcess()
val version = BufferedReader(InputStreamReader(process.inputStream)).readLine()
val regResult = Regex("v([0-9]+)\\.([0-9]+)\\.([0-9]+)").find(version)
if (regResult != null && regResult.groupValues[1].toInt() >= 16) {
return
} else {
throw AgentException("Node version is too old: $version. Please install Node.js v16+ and add bin path to system environment variable PATH, then restart IDE.")
}
} catch (e: Exception) {
if (e is AgentException) {
throw e
} else {
throw AgentException("Failed to check node version: $e. Please check your node installation.")
}
}
}
data class Config( data class Config(
val server: Server? = null, val server: Server? = null,
val completion: Completion? = null, val completion: Completion? = null,
@ -191,7 +212,7 @@ class Agent : ProcessAdapter() {
} }
suspend fun postEvent(event: LogEventRequest) { suspend fun postEvent(event: LogEventRequest) {
return request("postEvent", listOf(event)) request<Any>("postEvent", listOf(event))
} }
data class AuthUrlResponse( data class AuthUrlResponse(
@ -200,8 +221,12 @@ class Agent : ProcessAdapter() {
) )
fun close() { fun close() {
streamWriter.close() try {
process.killProcess() streamWriter.close()
process.killProcess()
} catch (e: Exception) {
// ignore
}
} }
private var requestId = 1 private var requestId = 1
@ -249,11 +274,11 @@ class Agent : ProcessAdapter() {
val data = try { val data = try {
gson.fromJson(output, Array::class.java).toList() gson.fromJson(output, Array::class.java).toList()
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Failed to parse agent output: $output") logger.warn("Failed to parse agent output: $output")
return return
} }
if (data.size != 2 || data[0] !is Number) { if (data.size != 2 || data[0] !is Number) {
logger.error("Failed to parse agent output: $output") logger.warn("Failed to parse agent output: $output")
return return
} }
logger.info("Parsed agent output: $data") logger.info("Parsed agent output: $data")
@ -302,7 +327,7 @@ class Agent : ProcessAdapter() {
} }
else -> { else -> {
logger.error("Agent notification, unknown event name: ${event["event"]}") logger.warn("Agent notification, unknown event name: ${event["event"]}")
} }
} }
} }

View File

@ -8,7 +8,6 @@ import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications import com.intellij.notification.Notifications
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ReadAction import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.invokeLater import com.intellij.openapi.application.invokeLater
@ -20,14 +19,12 @@ import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.tabbyml.intellijtabby.actions.OpenAuthPage
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import com.tabbyml.intellijtabby.usage.AnonymousUsageLogger import com.tabbyml.intellijtabby.usage.AnonymousUsageLogger
import io.ktor.util.* import io.ktor.util.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Service @Service
@ -35,11 +32,27 @@ class AgentService : Disposable {
private val logger = Logger.getInstance(AgentService::class.java) private val logger = Logger.getInstance(AgentService::class.java)
private var agent: Agent = Agent() private var agent: Agent = Agent()
val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private var initFailedNotification: Notification? = null
var authNotification: Notification? = null var authNotification: Notification? = null
private set private set
var issueNotification: Notification? = null var issueNotification: Notification? = null
private set private set
val status get() = agent.status
enum class Status {
INITIALIZING,
INITIALIZATION_FAILED,
}
private var initResultFlow: MutableStateFlow<Boolean?> = MutableStateFlow(null)
val status get() = initResultFlow.combine(agent.status) { initResult, agentStatus ->
if (initResult == null) {
Status.INITIALIZING
} else if (initResult) {
agentStatus
} else {
Status.INITIALIZATION_FAILED
}
}.stateIn(scope, SharingStarted.WhileSubscribed(), Status.INITIALIZING)
val currentIssue get() = agent.currentIssue val currentIssue get() = agent.currentIssue
init { init {
@ -54,14 +67,28 @@ class AgentService : Disposable {
try { try {
agent.open() agent.open()
agent.initialize(createAgentConfig(settings.data), client) agent.initialize(createAgentConfig(settings.data), client)
initResultFlow.value = true
logger.info("Agent init done.") logger.info("Agent init done.")
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Agent init failed: $e") initResultFlow.value = false
logger.warn("Agent init failed: $e")
anonymousUsageLogger.event( anonymousUsageLogger.event(
"IntelliJInitFailed", mapOf( "IntelliJInitFailed", mapOf(
"client" to client, "error" to e.stackTraceToString() "client" to client, "error" to e.stackTraceToString()
) )
) )
val notification = Notification(
"com.tabbyml.intellijtabby.notification.warning",
"Tabby initialization failed",
"${e.message}",
NotificationType.ERROR,
)
// FIXME: Add action to open FAQ page to help user set up nodejs.
invokeLater {
initFailedNotification?.expire()
initFailedNotification = notification
Notifications.Bus.notify(notification)
}
} }
} }
@ -181,7 +208,7 @@ class AgentService : Disposable {
BrowserUtil.browse(authUrlResponse.authUrl) BrowserUtil.browse(authUrlResponse.authUrl)
progress.text = "Waiting for authorization from browser..." progress.text = "Waiting for authorization from browser..."
agent.waitForAuthToken(authUrlResponse.code) agent.waitForAuthToken(authUrlResponse.code)
if (status.value == Agent.Status.READY) { if (agent.status.value == Agent.Status.READY) {
Notification( Notification(
"com.tabbyml.intellijtabby.notification.info", "com.tabbyml.intellijtabby.notification.info",
"Congrats, you're authorized, start to use Tabby now.", "Congrats, you're authorized, start to use Tabby now.",

View File

@ -13,7 +13,7 @@ class ApplicationSettingsPanel {
val mainPanel: JPanel = FormBuilder.createFormBuilder() val mainPanel: JPanel = FormBuilder.createFormBuilder()
.addLabeledComponent("Server endpoint", serverEndpointTextField, 1, false) .addLabeledComponent("Server endpoint", serverEndpointTextField, 1, false)
.addTooltip("A http or https URL of Tabby server endpoint.") .addTooltip("A http or https URL of Tabby server endpoint.")
.addTooltip("If leave empty, server endpoint config in `~/.tabby/agent/config.toml` will be used") .addTooltip("If leave empty, server endpoint config in `~/.tabby-client/agent/config.toml` will be used")
.addTooltip("Default to 'http://localhost:8080'.") .addTooltip("Default to 'http://localhost:8080'.")
.addSeparator() .addSeparator()
.addComponent(isAutoCompletionEnabledCheckBox, 1) .addComponent(isAutoCompletionEnabledCheckBox, 1)

View File

@ -86,16 +86,20 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
) )
} }
private fun updateStatus(settingsState: ApplicationSettingsState.State, agentStatus: Agent.Status, currentIssue: String?) { private fun updateStatus(settingsState: ApplicationSettingsState.State, agentStatus: Enum<*>, currentIssue: String?) {
if (!settingsState.isAutoCompletionEnabled) { if (!settingsState.isAutoCompletionEnabled) {
icon = AllIcons.Windows.CloseSmall icon = AllIcons.Windows.CloseSmall
tooltip = "Tabby: Auto completion is disabled" tooltip = "Tabby: Auto completion is disabled"
} else { } else {
when(agentStatus) { when(agentStatus) {
Agent.Status.NOT_INITIALIZED -> { AgentService.Status.INITIALIZING, Agent.Status.NOT_INITIALIZED -> {
icon = AllIcons.Actions.Refresh icon = AllIcons.Actions.Refresh
tooltip = "Tabby: Initializing" tooltip = "Tabby: Initializing"
} }
AgentService.Status.INITIALIZATION_FAILED -> {
icon = AllIcons.General.Error
tooltip = "Tabby: Initialization failed"
}
Agent.Status.READY -> { Agent.Status.READY -> {
icon = AllIcons.Actions.Checked icon = AllIcons.Actions.Checked
tooltip = "Tabby: Ready" tooltip = "Tabby: Ready"

View File

@ -34,13 +34,13 @@ class AnonymousUsageLogger {
try { try {
val home = System.getProperty("user.home") val home = System.getProperty("user.home")
logger.info("User home: $home") logger.info("User home: $home")
val datafile = Path(home).resolve(".tabby/agent/data.json").toFile() val datafile = Path(home).resolve(".tabby-client/agent/data.json").toFile()
var data: Map<*, *>? = null var data: Map<*, *>? = null
try { try {
val dataJson = datafile.inputStream().bufferedReader().use { it.readText() } val dataJson = datafile.inputStream().bufferedReader().use { it.readText() }
data = gson.fromJson(dataJson, Map::class.java) data = gson.fromJson(dataJson, Map::class.java)
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Failed to load anonymous ID: ${e.message}") logger.info("Failed to load anonymous ID: ${e.message}")
} }
if (data?.get("anonymousId") != null) { if (data?.get("anonymousId") != null) {
anonymousId = data["anonymousId"].toString() anonymousId = data["anonymousId"].toString()
@ -50,11 +50,12 @@ class AnonymousUsageLogger {
val newData = data?.toMutableMap() ?: mutableMapOf() val newData = data?.toMutableMap() ?: mutableMapOf()
newData["anonymousId"] = anonymousId newData["anonymousId"] = anonymousId
val newDataJson = gson.toJson(newData) val newDataJson = gson.toJson(newData)
datafile.parentFile.mkdirs()
datafile.writeText(newDataJson) datafile.writeText(newDataJson)
logger.info("Create new anonymous ID: $anonymousId") logger.info("Create new anonymous ID: $anonymousId")
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Failed when init anonymous ID: ${e.message}") logger.warn("Failed when init anonymous ID: ${e.message}")
anonymousId = UUID.randomUUID().toString() anonymousId = UUID.randomUUID().toString()
} finally { } finally {
initialized.value = true initialized.value = true
@ -97,13 +98,13 @@ class AnonymousUsageLogger {
val responseCode = connection.responseCode val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
logger.info("Usage event sent successfully.") logger.info("Usage event sent successfully: $requestString")
} else { } else {
logger.error("Usage event failed to send: $responseCode") logger.warn("Usage event failed to send: $responseCode")
} }
connection.disconnect() connection.disconnect()
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Usage event failed to send: ${e.message}") logger.warn("Usage event failed to send: ${e.message}")
} }
} }