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.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"]}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue