fix(intellij): fix exception handling when initializing tabby. (#429)
parent
5f6cbcaf94
commit
31217bcfc8
File diff suppressed because one or more lines are too long
|
|
@ -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"]}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue