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.asStateFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
class Agent : ProcessAdapter() {
@ -52,18 +54,18 @@ class Agent : ProcessAdapter() {
if (node?.exists() == true) {
logger.info("Node bin path: ${node.absolutePath}")
} else {
logger.error("Node bin not found")
throw AgentException("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.")
}
checkNodeVersion(node.absolutePath)
val script =
PluginManagerCore.getPlugin(PluginId.getId("com.tabbyml.intellij-tabby"))?.pluginPath?.resolve("node_scripts/tabby-agent.js")
?.toFile()
if (script?.exists() == true) {
logger.info("Node script path: ${script.absolutePath}")
} else {
logger.error("Node script not found")
throw AgentException("Node script not found")
throw AgentException("Node script not found. Please reinstall Tabby plugin.")
}
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
@ -77,6 +79,25 @@ class Agent : ProcessAdapter() {
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(
val server: Server? = null,
val completion: Completion? = null,
@ -191,7 +212,7 @@ class Agent : ProcessAdapter() {
}
suspend fun postEvent(event: LogEventRequest) {
return request("postEvent", listOf(event))
request<Any>("postEvent", listOf(event))
}
data class AuthUrlResponse(
@ -200,8 +221,12 @@ class Agent : ProcessAdapter() {
)
fun close() {
try {
streamWriter.close()
process.killProcess()
} catch (e: Exception) {
// ignore
}
}
private var requestId = 1
@ -249,11 +274,11 @@ class Agent : ProcessAdapter() {
val data = try {
gson.fromJson(output, Array::class.java).toList()
} catch (e: Exception) {
logger.error("Failed to parse agent output: $output")
logger.warn("Failed to parse agent output: $output")
return
}
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
}
logger.info("Parsed agent output: $data")
@ -302,7 +327,7 @@ class Agent : ProcessAdapter() {
}
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.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ReadAction
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.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.tabbyml.intellijtabby.actions.OpenAuthPage
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import com.tabbyml.intellijtabby.usage.AnonymousUsageLogger
import io.ktor.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@Service
@ -35,11 +32,27 @@ class AgentService : Disposable {
private val logger = Logger.getInstance(AgentService::class.java)
private var agent: Agent = Agent()
val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private var initFailedNotification: Notification? = null
var authNotification: Notification? = null
private set
var issueNotification: Notification? = null
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
init {
@ -54,14 +67,28 @@ class AgentService : Disposable {
try {
agent.open()
agent.initialize(createAgentConfig(settings.data), client)
initResultFlow.value = true
logger.info("Agent init done.")
} catch (e: Exception) {
logger.error("Agent init failed: $e")
initResultFlow.value = false
logger.warn("Agent init failed: $e")
anonymousUsageLogger.event(
"IntelliJInitFailed", mapOf(
"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)
progress.text = "Waiting for authorization from browser..."
agent.waitForAuthToken(authUrlResponse.code)
if (status.value == Agent.Status.READY) {
if (agent.status.value == Agent.Status.READY) {
Notification(
"com.tabbyml.intellijtabby.notification.info",
"Congrats, you're authorized, start to use Tabby now.",

View File

@ -13,7 +13,7 @@ class ApplicationSettingsPanel {
val mainPanel: JPanel = FormBuilder.createFormBuilder()
.addLabeledComponent("Server endpoint", serverEndpointTextField, 1, false)
.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'.")
.addSeparator()
.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) {
icon = AllIcons.Windows.CloseSmall
tooltip = "Tabby: Auto completion is disabled"
} else {
when(agentStatus) {
Agent.Status.NOT_INITIALIZED -> {
AgentService.Status.INITIALIZING, Agent.Status.NOT_INITIALIZED -> {
icon = AllIcons.Actions.Refresh
tooltip = "Tabby: Initializing"
}
AgentService.Status.INITIALIZATION_FAILED -> {
icon = AllIcons.General.Error
tooltip = "Tabby: Initialization failed"
}
Agent.Status.READY -> {
icon = AllIcons.Actions.Checked
tooltip = "Tabby: Ready"

View File

@ -34,13 +34,13 @@ class AnonymousUsageLogger {
try {
val home = System.getProperty("user.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
try {
val dataJson = datafile.inputStream().bufferedReader().use { it.readText() }
data = gson.fromJson(dataJson, Map::class.java)
} 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) {
anonymousId = data["anonymousId"].toString()
@ -50,11 +50,12 @@ class AnonymousUsageLogger {
val newData = data?.toMutableMap() ?: mutableMapOf()
newData["anonymousId"] = anonymousId
val newDataJson = gson.toJson(newData)
datafile.parentFile.mkdirs()
datafile.writeText(newDataJson)
logger.info("Create new anonymous ID: $anonymousId")
}
} 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()
} finally {
initialized.value = true
@ -97,13 +98,13 @@ class AnonymousUsageLogger {
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
logger.info("Usage event sent successfully.")
logger.info("Usage event sent successfully: $requestString")
} else {
logger.error("Usage event failed to send: $responseCode")
logger.warn("Usage event failed to send: $responseCode")
}
connection.disconnect()
} catch (e: Exception) {
logger.error("Usage event failed to send: ${e.message}")
logger.warn("Usage event failed to send: ${e.message}")
}
}