Skip to content

Commit

Permalink
Merge pull request #6 from bluelhf/main
Browse files Browse the repository at this point in the history
Add inactivity state and fix hostname bug
  • Loading branch information
bluelhf authored Feb 28, 2023
2 parents db293c4 + 8a70366 commit c0308b8
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 94 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Project name is now correctly displayed
for projects in directories with different names.

### Changed
- Coding time no longer increases while inactive.
- Hostname now works regardless of DNS settings.\*

*Works on Windows, macOS, and any OS with `gethostname()`.

## [0.2.0] - 2022-08-04

### Added
Expand Down
11 changes: 6 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ plugins {
// Java support
id("java")
// Kotlin support
id("org.jetbrains.kotlin.jvm") version "1.7.10"
id("org.jetbrains.kotlin.jvm") version "1.8.10"
// Gradle IntelliJ Plugin
id("org.jetbrains.intellij") version "1.7.0"
id("org.jetbrains.intellij") version "1.13.0"
// Gradle Changelog Plugin
id("org.jetbrains.changelog") version "1.3.1"
id("org.jetbrains.changelog") version "2.0.0"
// Gradle Qodana Plugin
id("org.jetbrains.qodana") version "0.1.13"
}
Expand All @@ -20,7 +20,8 @@ group = properties("pluginGroup")
version = properties("pluginVersion")

dependencies {
implementation("com.google.code.gson:gson:2.9.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.kstruct:gethostname4j:1.0.0")
}

// Configure project's dependencies
Expand Down Expand Up @@ -71,7 +72,7 @@ tasks {
patchPluginXml {
version.set(properties("pluginVersion"))
sinceBuild.set(properties("pluginSinceBuild"))
untilBuild.set(properties("pluginUntilBuild"))
// untilBuild.set(properties("pluginUntilBuild"))

// Extract the <!-- Plugin description --> section from README.md and provide for the plugin's manifest
pluginDescription.set(
Expand Down
5 changes: 2 additions & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
pluginGroup = fi.testaustime.plugin_intellij
pluginName = testaustime-intellij
# SemVer format -> https://semver.org
pluginVersion = 0.2.0
pluginVersion = 0.3.0

# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild = 212.4746.92
pluginUntilBuild = 282.*


# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
Expand All @@ -24,7 +23,7 @@ platformPlugins = org.jetbrains.kotlin
javaVersion = 17

# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 7.5
gradleVersion = 8.0.1

# Opt-out flag for bundling Kotlin standard library.
# See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details.
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
package fi.testaustime.plugin_intellij.network.models

import com.google.gson.annotations.SerializedName
import com.intellij.openapi.project.Project
import fi.testaustime.plugin_intellij.utils.ContextInformation
import fi.testaustime.plugin_intellij.utils.ContextInformation.getFriendlyName

class ActivityPostPayload(
data class ActivityPostPayload(
@SerializedName("hostname") val hostname: String?,
@SerializedName("language") val programmingLanguage: String?,
@SerializedName("hostname") val host: String,
@SerializedName("editor_name") val IDEName: String,
@SerializedName("project_name") val projectName: String,
)
@SerializedName("editor_name") val application: String,
) {
companion object {
fun fromProject(project: Project): ActivityPostPayload {
return ActivityPostPayload(
hostname = ContextInformation.getHostname(),
programmingLanguage = ContextInformation.getProgrammingLanguage(),
application = ContextInformation.getApplicationName(),
projectName = project.getFriendlyName()
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fi.testaustime.plugin_intellij.services

import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.EditorFactoryEvent
import com.intellij.openapi.editor.event.EditorFactoryListener
import java.time.Duration
import java.time.Duration.ofSeconds
import java.time.Instant
import java.time.Instant.now

class CaretMoveListener(service: TestaustimeProjectService) : com.intellij.openapi.editor.event.CaretListener {
companion object {
val TIMEOUT: Duration = ofSeconds(30)
}

init {
EditorFactory.getInstance().addEditorFactoryListener(object : EditorFactoryListener {
override fun editorCreated(event: EditorFactoryEvent) {
event.editor.caretModel.addCaretListener(this@CaretMoveListener)
}
}, service)
}

private var lastActive: Instant = now()

override fun caretPositionChanged(event: CaretEvent) {
lastActive = now()
}

fun isActive(): Boolean {
return Duration.between(lastActive, now()) <= TIMEOUT
}
}
Original file line number Diff line number Diff line change
@@ -1,137 +1,124 @@
package fi.testaustime.plugin_intellij.services

import com.intellij.ide.DataManager
import com.intellij.notification.NotificationType.ERROR
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.util.PsiUtilBase
import com.intellij.util.concurrency.AppExecutorUtil
import fi.testaustime.plugin_intellij.TestaustimeBundle
import fi.testaustime.plugin_intellij.TestaustimeBundle.message
import fi.testaustime.plugin_intellij.configuration.SettingsState
import fi.testaustime.plugin_intellij.network.TestaustimeAPIClient
import fi.testaustime.plugin_intellij.network.models.ActivityPostPayload
import fi.testaustime.plugin_intellij.utils.ContextInformation
import fi.testaustime.plugin_intellij.utils.TestaustimeNotifier
import java.net.InetAddress
import java.net.http.HttpResponse
import java.util.concurrent.CompletableFuture
import java.time.Duration
import java.time.Duration.ofSeconds
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.MILLISECONDS

class TestaustimeApplicationService {
companion object {
private val HEARTBEAT_PERIOD: Duration = ofSeconds(30);
}

init {
Logger.getInstance("#Testaustime").debug(message("applicationService.started"))
startScheduledPinger();
startScheduledPinger()
}

private var scheduledPingTask: ScheduledFuture<*>? = null;
private var scheduledPingTask: ScheduledFuture<*>? = null

// Track last project to know when to flush sessions
private var lastProject: Project? = null;
private var lastProject: Project? = null

// API token invalidation state, used for notification tracking
private var wasInvalid: Boolean = true;
private var isInvalid: Boolean = false;

// Past connection exception state, used for de-duping connection exception notifications
private var didFail: Boolean = false;
private var didFail: Boolean = false

fun terminateService() {
flushActivity();
flushActivity()
scheduledPingTask?.cancel(true)
Logger.getInstance("#Testaustime").debug(message("applicationService.terminated"))
}

private fun flushActivity() {
val settings = SettingsState.instance;
val client = TestaustimeAPIClient(settings.apiBaseUrl, settings.authToken);
client.flushActivity().join();
val settings = SettingsState.instance
val client = TestaustimeAPIClient(settings.apiBaseUrl, settings.authToken)
client.flushActivity().join()
}

private fun startScheduledPinger() {
Logger.getInstance("#Testaustime").debug("Call for schedule ping registration")
if (scheduledPingTask != null) return
scheduledPingTask = AppExecutorUtil.getAppScheduledExecutorService().scheduleWithFixedDelay({
pingNow()
}, 0, 30, TimeUnit.SECONDS)
}, 0, HEARTBEAT_PERIOD.toMillis(), MILLISECONDS)
Logger.getInstance("#Testaustime").debug(
message("applicationService.pinger.registered")
)
}

fun pingNow() {
val settings = SettingsState.instance;
val settings = SettingsState.instance

try {
val client = TestaustimeAPIClient(settings.apiBaseUrl, settings.authToken);

val hostname = InetAddress.getLocalHost().hostName
val appName = ApplicationInfo.getInstance().fullApplicationName
val dataContext = DataManager.getInstance().dataContextFromFocusAsync
dataContext.onSuccess { ctx ->
if (isInvalid) {
wasInvalid = true;
return@onSuccess;
val client = TestaustimeAPIClient(settings.apiBaseUrl, settings.authToken)

ApplicationManager.getApplication().invokeLater({
val ctx = ContextInformation.getDataContext() ?: return@invokeLater;
val project = ctx.getData(CommonDataKeys.PROJECT)
project ?: return@invokeLater

// Consider switching projects to be a new session
if (lastProject?.equals(project) == false) {
flushActivity()
lastProject = project
}

ctx.getData(PlatformDataKeys.PROJECT)?.let { project ->
// Consider switching projects to be a new session
if (lastProject?.equals(project) == false) {
flushActivity()
lastProject = project
val editor: Editor? = ctx.getData(PlatformDataKeys.EDITOR)
val service = project.getService(TestaustimeProjectService::class.java)
val future = if (fun(): Boolean {
if (!project.isOpen) return false
if (settings.authToken.isBlank()) return false
if (!service.isActive()) return false;
editor ?: return false

return true
}.invoke()) {
client.activityLog(ActivityPostPayload.fromProject(project))
} else client.me()

future.thenAccept { resp ->
if (resp.statusCode() == 401) {
TestaustimeProjectService.broadcast(false)
return@thenAccept
} else if (didFail) {
TestaustimeProjectService.broadcast(true)
didFail = false
}

val editor: Editor? = ctx.getData(PlatformDataKeys.EDITOR)
val type: String?

val future: CompletableFuture<HttpResponse<String>>
if (project.isOpen && editor != null && settings.authToken.isNotBlank()) {
type = PsiUtilBase.getPsiFileInEditor(editor, project)?.fileType?.displayName
future = client.activityLog(
ActivityPostPayload(
programmingLanguage = type,
projectName = project.name,
IDEName = appName,
host = hostname
)
);
} else {
future = client.me();
}.exceptionally {
if (!didFail) {
TestaustimeNotifier.notification(
ERROR, null,
message("applicationService.heartbeat.failed"), it.localizedMessage
)

didFail = true
}

future.thenAccept { resp ->
if (resp.statusCode() == 401) {
TestaustimeProjectService.broadcast(false)
isInvalid = true
wasInvalid = true
return@thenAccept
} else if (wasInvalid || didFail) {
TestaustimeProjectService.broadcast(true)
wasInvalid = false
didFail = false;
}

}.exceptionally {
if (!didFail) {
TestaustimeNotifier.notification(
ERROR, null,
message("applicationService.heartbeat.failed"), it.localizedMessage
)
didFail = true;
}
return@exceptionally null;
}
return@exceptionally null
}
}
dataContext.onError { err ->
Logger.getInstance("#Testaustime").error(err)
}
} catch (e: Exception) {
}, ModalityState.any())
} catch (e: Throwable) {
TestaustimeNotifier.notification(
ERROR, null,
message("applicationService.heartbeat.failed"), e.localizedMessage
)

Logger.getInstance("#Testaustime").error(e)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
package fi.testaustime.plugin_intellij.services

import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import fi.testaustime.plugin_intellij.TestaustimeBundle.message
import fi.testaustime.plugin_intellij.utils.ContextInformation.getFriendlyName
import fi.testaustime.plugin_intellij.utils.TestaustimeNotifier


class TestaustimeProjectService(private val project: Project) {
class TestaustimeProjectService(private val project: Project) : Disposable {
private val caretListener: CaretMoveListener

init {
projects.add(project);
caretListener = CaretMoveListener(this);
}

fun terminate() {
projects.remove(project);
}

fun isActive(): Boolean {
return caretListener.isActive()
}

companion object {
var projects: MutableList<Project> = ArrayList();
fun broadcast(tokenValid: Boolean) {
for (project in projects) {
if (tokenValid) {
TestaustimeNotifier.notifyInfo(project, message("projectService.active.title"),
message("projectService.active.message", project.name));
message("projectService.active.message", project.getFriendlyName()));
} else {
TestaustimeNotifier.notification(NotificationType.ERROR, project, message("projectService.invalid.title"),
message("projectService.invalid.message", project.name))
}
}
}
}

override fun dispose() {
terminate()
}
}
Loading

0 comments on commit c0308b8

Please sign in to comment.