Compare commits

...
Author SHA1 Message Date
dev-claw 5d251d196f fixes #228 2025-04-13 19:42:26 +01:00
45 changed files with 940 additions and 531 deletions
@@ -41,16 +41,16 @@ val coreModule = module {
DataTransaction(get(), get(), get(), get(), get(), get())
}
single<RetryPolicyService> {
RetryPolicyService(get(), get())
RetryPolicyService()
}
single<HTTPService> {
HTTPService(get())
HTTPService()
}
single<VGAuthService> {
VGAuthService(get(), get())
}
single<ThreadCacheService> {
ThreadCacheService(get(), get())
ThreadCacheService(get())
}
single<DownloadService> {
DownloadService(get(), get(), get(), get())
@@ -0,0 +1,59 @@
package me.vripper.listeners
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.services.*
import me.vripper.utilities.DatabaseManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
object AppManager : KoinComponent {
private val eventBus: EventBus by inject()
private val dataTransaction: DataTransaction by inject()
private val metadataService: MetadataService by inject()
private val settingsService: SettingsService by inject()
private val vgAuthService: VGAuthService by inject()
private val downloadSpeedService: DownloadSpeedService by inject()
private val httpService: HTTPService by inject()
private val retryPolicyService: RetryPolicyService by inject()
private val threadCacheService: ThreadCacheService by inject()
private val downloadService: DownloadService by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var job: Job? = null
fun start() {
job?.cancel()
job = coroutineScope.launch {
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
httpService.client.close()
httpService.buildRequestConfig(it.settings.connectionSettings.timeout.toLong())
httpService.buildConnectionConfig(it.settings.connectionSettings.timeout.toLong())
httpService.buildConnectionPool()
httpService.buildClientBuilder()
vgAuthService.authenticate(it.settings)
retryPolicyService.maxAttempts = it.settings.connectionSettings.maxAttempts
threadCacheService.invalidate()
}
}
DatabaseManager.connect()
dataTransaction.setDownloadingToStopped()
dataTransaction.stopImagesByPostIdAndIsNotCompleted()
settingsService.init()
metadataService.fetchExisting()
downloadSpeedService.init()
downloadService.init()
}
fun stop() {
job?.cancel()
if (DatabaseManager.isConnected()) {
downloadService.halt()
downloadService.stop()
downloadSpeedService.halt()
DatabaseManager.disconnect()
}
}
}
@@ -1,30 +0,0 @@
package me.vripper.listeners
import me.vripper.services.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
open class OnStartupListener : KoinComponent {
private val dataTransaction: DataTransaction by inject()
private val metadataService: MetadataService by inject()
private val settingsService: SettingsService by inject()
private val vgAuthService: VGAuthService by inject()
private val downloadSpeedService: DownloadSpeedService by inject()
private val httpService: HTTPService by inject()
private val retryPolicyService: RetryPolicyService by inject()
private val threadCacheService: ThreadCacheService by inject()
private val downloadService: DownloadService by inject()
open fun run() {
dataTransaction.setDownloadingToStopped()
dataTransaction.stopImagesByPostIdAndIsNotCompleted()
vgAuthService.init()
httpService.init()
retryPolicyService.init()
threadCacheService.init()
settingsService.init()
metadataService.init()
downloadSpeedService.init()
downloadService.init()
}
}
@@ -169,10 +169,6 @@ internal class AppEndpointService(
}
}
override fun ready(): Boolean {
return true
}
override suspend fun rename(postId: Long, newName: String) {
taskRunner.submit {
synchronized(postId.toString().intern()) {
@@ -53,6 +53,8 @@ internal class DownloadService(
private val lock = ReentrantLock()
private val condition = lock.newCondition()
private var downloadMonitorThread: Thread? = null
internal class ImageDownloadContext(val imageEntity: ImageEntity, val settings: Settings) : KoinComponent {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val jobs = mutableListOf<Job>()
@@ -202,7 +204,8 @@ internal class DownloadService(
}
fun init() {
Thread.ofVirtual().name("Download Loop").unstarted(Runnable {
downloadMonitorThread?.interrupt()
downloadMonitorThread = Thread.ofVirtual().name("Download Monitor").unstarted(Runnable {
val accepted: MutableList<ImageDownloadRunnable> = mutableListOf()
val candidates: MutableList<ImageDownloadRunnable> = mutableListOf()
while (!Thread.currentThread().isInterrupted) {
@@ -228,7 +231,12 @@ internal class DownloadService(
}
}
}
}).start()
})
downloadMonitorThread?.start()
}
fun halt() {
downloadMonitorThread?.interrupt()
}
fun stop(postIds: List<Long> = emptyList()) {
@@ -19,9 +19,11 @@ internal class DownloadSpeedService(
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val bytesCount = AtomicLong(0)
private var job: Job? = null
private var queueStateUpdateJob: Job? = null
fun init() {
coroutineScope.launch {
queueStateUpdateJob?.cancel()
queueStateUpdateJob = coroutineScope.launch {
eventBus.events.filterIsInstance(QueueStateEvent::class).collect {
if (it.queueState.running + it.queueState.remaining > 0) {
if (job == null || job?.isActive == false) {
@@ -45,6 +47,11 @@ internal class DownloadSpeedService(
}
}
fun halt() {
queueStateUpdateJob?.cancel()
job?.cancel()
}
fun reportDownloadedBytes(count: Long) {
bytesCount.addAndGet(count)
}
@@ -1,9 +1,6 @@
package me.vripper.services
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.utilities.ApplicationProperties
import org.apache.hc.client5.http.config.ConnectionConfig
import org.apache.hc.client5.http.config.RequestConfig
@@ -18,36 +15,17 @@ import org.apache.hc.core5.pool.PoolConcurrencyPolicy
import org.apache.hc.core5.pool.PoolReusePolicy
import org.apache.hc.core5.util.Timeout
internal class HTTPService(
private val eventBus: EventBus
) {
internal class HTTPService {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var pcm: HttpClientConnectionManager = BasicHttpClientConnectionManager()
private var rc: RequestConfig = RequestConfig.DEFAULT
private var cc: ConnectionConfig = ConnectionConfig.DEFAULT
private var connectionTimeout = 30
private var connectionExpiryJob: Job? = null
var client: CloseableHttpClient = HttpClients.createDefault()
fun init() {
coroutineScope.launch {
eventBus
.events
.filterIsInstance(SettingsUpdateEvent::class)
.collect {
connectionTimeout = it.settings.connectionSettings.timeout
client.close()
pcm.close()
buildRequestConfig()
buildConnectionConfig()
buildConnectionPool()
buildClientBuilder()
}
}
}
private fun buildConnectionPool() {
fun buildConnectionPool() {
pcm.close()
connectionExpiryJob?.cancel()
pcm = PoolingHttpClientConnectionManagerBuilder.create()
.setPoolConcurrencyPolicy(PoolConcurrencyPolicy.LAX)
@@ -65,21 +43,21 @@ internal class HTTPService(
}
}
private fun buildRequestConfig() {
fun buildRequestConfig(connectionTimeout: Long) {
rc = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(connectionTimeout.toLong()))
.setConnectionRequestTimeout(Timeout.ofSeconds(connectionTimeout))
.setCookieSpec(StandardCookieSpec.RELAXED)
.build()
}
private fun buildConnectionConfig() {
fun buildConnectionConfig(connectionTimeout: Long) {
cc = ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(connectionTimeout.toLong()))
.setSocketTimeout(Timeout.ofSeconds(connectionTimeout.toLong()))
.setConnectTimeout(Timeout.ofSeconds(connectionTimeout))
.setSocketTimeout(Timeout.ofSeconds(connectionTimeout))
.build()
}
private fun buildClientBuilder() {
fun buildClientBuilder() {
client = HttpClients.custom()
.setConnectionManager(pcm)
.setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
@@ -42,7 +42,6 @@ interface IAppEndpointService {
suspend fun loggedInUser(): String
suspend fun getVersion(): String
suspend fun renameToFirst(postIds: List<Long>)
fun ready(): Boolean
suspend fun dbMigration(): String
suspend fun initLogger()
}
@@ -8,7 +8,7 @@ internal class MetadataService(
private val settingsService: SettingsService,
) {
fun init() {
fun fetchExisting() {
if (!settingsService.settings.viperSettings.fetchMetadata) {
return
}
@@ -1,32 +1,12 @@
package me.vripper.services
import dev.failsafe.RetryPolicy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.utilities.LoggerDelegate
import java.time.temporal.ChronoUnit
internal class RetryPolicyService(
val eventBus: EventBus, settingsService: SettingsService
) {
internal class RetryPolicyService {
private val log by LoggerDelegate()
private var maxAttempts: Int = settingsService.settings.connectionSettings.maxAttempts
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun init() {
coroutineScope.launch {
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
if (maxAttempts != it.settings.connectionSettings.maxAttempts) {
maxAttempts = it.settings.connectionSettings.maxAttempts
}
}
}
}
var maxAttempts: Int = 3
fun <T> buildRetryPolicy(message: String): RetryPolicy<T> {
return RetryPolicy.builder<T>().withDelay(2, 5, ChronoUnit.SECONDS).withMaxAttempts(maxAttempts)
@@ -2,29 +2,12 @@ package me.vripper.services
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.vgapi.ThreadItem
import me.vripper.vgapi.ThreadLookupAPIParser
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
internal class ThreadCacheService(val eventBus: EventBus, val dataTransaction: DataTransaction) {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun init() {
coroutineScope.launch {
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
cache.invalidateAll()
}
}
}
internal class ThreadCacheService(val dataTransaction: DataTransaction) {
private val cache: LoadingCache<Long, ThreadItem> =
Caffeine.newBuilder().expireAfterWrite(20, TimeUnit.MINUTES).build { threadId ->
@@ -37,6 +20,10 @@ internal class ThreadCacheService(val eventBus: EventBus, val dataTransaction: D
threadItem
}
fun invalidate() {
cache.invalidateAll()
}
@Throws(ExecutionException::class)
operator fun get(threadId: Long): ThreadItem {
return cache[threadId]
@@ -1,13 +1,7 @@
package me.vripper.services
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import me.vripper.entities.PostEntity
import me.vripper.event.EventBus
import me.vripper.event.SettingsUpdateEvent
import me.vripper.event.VGUserLoginEvent
import me.vripper.exception.VripperException
import me.vripper.model.Settings
@@ -27,21 +21,12 @@ internal class VGAuthService(
private val cm: HTTPService,
private val eventBus: EventBus,
) {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val log by LoggerDelegate()
private val vgCookies: MutableList<Cookie> = mutableListOf()
var loggedUser = ""
private var authenticated = false
fun init() {
coroutineScope.launch {
eventBus.events.filterIsInstance(SettingsUpdateEvent::class).collect {
authenticate(it.settings)
}
}
}
private fun authenticate(settings: Settings) {
fun authenticate(settings: Settings) {
if (!settings.viperSettings.login) {
log.debug("Authentication option is disabled")
authenticated = false
@@ -120,6 +105,7 @@ internal class VGAuthService(
authenticated = true
loggedUser = username
eventBus.publishEvent(VGUserLoginEvent(loggedUser))
log.info("Successfully logged in as: $loggedUser")
}
fun leaveThanks(postEntity: PostEntity) {
@@ -42,7 +42,7 @@ internal class LeaveThanksTask(
)
}
RequestLimit.getPermit(1)
log.debug("Requesting {}", postThanks.uri)
log.info("Posting {}", postThanks.uri)
cm.client.execute(postThanks, context) { response ->
if (response.code / 100 != 2) {
throw VripperException("Unexpected response code '${response.code}' for $postThanks")
@@ -0,0 +1,72 @@
package me.vripper.utilities
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.concurrent.locks.ReentrantLock
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.concurrent.withLock
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class)
object AesUtils {
val mutex = ReentrantLock()
val secureRandom: SecureRandom = SecureRandom()
val cipher: Cipher = Cipher.getInstance(AES_ALGORITHM_GCM)
const val SHA_CRYPT: String = "SHA-256"
const val AES_ALGORITHM: String = "AES"
const val AES_ALGORITHM_GCM: String = "AES/GCM/NoPadding"
const val IV_LENGTH_ENCRYPT: Int = 12
const val TAG_LENGTH_ENCRYPT: Int = 16
fun aesDecrypt(data: ByteArray, passPhrase: String): ByteArray {
val key = generateAesKeyFromPassphrase(passPhrase)
val iv = ByteArray(IV_LENGTH_ENCRYPT)
System.arraycopy(data, 0, iv, 0, iv.size)
val encryptedText = ByteArray(data.size - IV_LENGTH_ENCRYPT)
System.arraycopy(data, IV_LENGTH_ENCRYPT, encryptedText, 0, encryptedText.size)
val gcmSpec = GCMParameterSpec(TAG_LENGTH_ENCRYPT * 8, iv)
val decryptedBytes = mutex.withLock {
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
cipher.doFinal(encryptedText)
}
return decryptedBytes
}
fun aesEncrypt(data: ByteArray, passPhrase: String): ByteArray {
val iv = ByteArray(IV_LENGTH_ENCRYPT)
secureRandom.nextBytes(iv)
val key = generateAesKeyFromPassphrase(passPhrase)
val gcmSpec = GCMParameterSpec(TAG_LENGTH_ENCRYPT * 8, iv)
val encryptedBytes = mutex.withLock {
cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec)
cipher.doFinal(data)
}
// Combine IV and encrypted text and encode them as Base64
val combinedIvAndCipherText = ByteArray(iv.size + encryptedBytes.size)
System.arraycopy(iv, 0, combinedIvAndCipherText, 0, iv.size)
System.arraycopy(encryptedBytes, 0, combinedIvAndCipherText, iv.size, encryptedBytes.size)
return combinedIvAndCipherText
}
fun generateAesKeyFromPassphrase(passPhrase: String): SecretKeySpec {
val sha256 = MessageDigest.getInstance(SHA_CRYPT)
val keyBytes = sha256.digest(passPhrase.toByteArray(StandardCharsets.UTF_8))
return SecretKeySpec(keyBytes, AES_ALGORITHM)
}
}
@@ -9,21 +9,35 @@ import liquibase.resource.ClassLoaderResourceAccessor
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.TransactionManager
import java.sql.Connection
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
object DatabaseManager {
private var database: Database? = null
private var connected: Boolean = false
private var lock = ReentrantLock()
fun connect() {
database =
Database.connect("jdbc:sqlite:${ApplicationProperties.VRIPPER_DIR}/vripper.db")
.also { update(it.connector.invoke().connection as Connection) }
lock.withLock {
database =
Database.connect("jdbc:sqlite:${ApplicationProperties.VRIPPER_DIR}/vripper.db")
.also { update(it.connector.invoke().connection as Connection) }
connected = true
}
}
fun disconnect() {
database?.let { TransactionManager.closeAndUnregister(it) }
lock.withLock {
database?.let { TransactionManager.closeAndUnregister(it) }
connected = false
}
}
fun update(connection: Connection) {
fun isConnected(): Boolean {
return lock.withLock { connected }
}
private fun update(connection: Connection) {
connection.use { cn ->
val database: liquibase.database.Database = DatabaseFactory.getInstance()
.findCorrectDatabaseImplementation(JdbcConnection(cn))
@@ -2,9 +2,6 @@ package me.vripper.gui.components.fragments
import atlantafx.base.theme.Styles
import atlantafx.base.util.IntegerStringConverter
import io.grpc.ConnectivityState
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Pos
import javafx.scene.control.RadioButton
import javafx.scene.control.Spinner
@@ -14,11 +11,9 @@ import kotlinx.coroutines.*
import me.vripper.gui.VripperGuiApplication
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.listener.GuiStartupLister
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.DatabaseManager
import me.vripper.listeners.AppManager
import tornadofx.*
class SessionFragment : Fragment("Change Session") {
@@ -26,10 +21,7 @@ class SessionFragment : Fragment("Change Session") {
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val widgetsController: WidgetsController by inject()
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val appEndpointService: IAppEndpointService by di("localAppEndpointService")
private val toggleGroup = ToggleGroup()
private val disableConfirm = SimpleBooleanProperty(false)
private val message = SimpleStringProperty()
override val root = VBox().apply {
alignment = Pos.CENTER
padding = insets(all = 5)
@@ -60,19 +52,17 @@ class SessionFragment : Fragment("Change Session") {
IntegerStringConverter.createFor(this)
})
}
field("Passcode") {
enableWhen { remoteRadio.selectedProperty() }
passwordfield(widgetsController.currentSettings.remoteSessionModel.passcodeProperty) {}
}
}
}
borderpane {
left {
padding = insets(all = 5.0)
label(message) {
disableWhen { message.isBlank() }
}
}
right {
padding = insets(all = 5.0)
button("Ok") {
enableWhen { toggleGroup.selectedToggleProperty().isNotNull.and(disableConfirm.not()) }
enableWhen { toggleGroup.selectedToggleProperty().isNotNull }
addClass(Styles.ACCENT)
isDefaultButton = true
action {
@@ -83,18 +73,14 @@ class SessionFragment : Fragment("Change Session") {
when ((selectedToggle as RadioButton).id) {
"localSession" -> {
coroutineScope.launch {
runLater {
disableConfirm.set(true)
message.set("Starting...")
}
AppManager.stop()
widgetsController.currentSettings.localSession = true
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
while (ActiveUICoroutines.all().isNotEmpty()) {
delay(200)
}
grpcEndpointService.disconnect()
DatabaseManager.connect()
GuiStartupLister().run()
AppManager.start()
GuiEventBus.publishEvent(GuiEventBus.LocalSession)
runLater {
close()
@@ -104,41 +90,20 @@ class SessionFragment : Fragment("Change Session") {
"remoteSession" -> {
coroutineScope.launch {
runLater {
disableConfirm.set(true)
message.set("Connecting...")
}
if (widgetsController.currentSettings.localSession) {
appEndpointService.stopAll()
}
AppManager.stop()
widgetsController.currentSettings.localSession = false
GuiEventBus.publishEvent(GuiEventBus.ChangingSession)
while (ActiveUICoroutines.all().isNotEmpty()) {
delay(200)
}
DatabaseManager.disconnect()
grpcEndpointService.disconnect()
grpcEndpointService.connect(
widgetsController.currentSettings.remoteSessionModel.host,
widgetsController.currentSettings.remoteSessionModel.port
widgetsController.currentSettings.remoteSessionModel.port,
widgetsController.currentSettings.remoteSessionModel.passcode,
)
repeat(10) {
if (grpcEndpointService.connectionState() == ConnectivityState.READY) {
GuiEventBus.publishEvent(GuiEventBus.RemoteSession)
runLater {
close()
}
cancel()
}
delay(333)
}
GuiEventBus.publishEvent(GuiEventBus.RemoteSession)
runLater {
runBlocking {
GuiEventBus.publishEvent(GuiEventBus.RemoteSessionFailure)
}
disableConfirm.set(false)
message.set("Unable to connect")
close()
}
}
}
@@ -1,5 +1,6 @@
package me.vripper.gui.components.views
import io.grpc.StatusException
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.geometry.Orientation
@@ -15,13 +16,11 @@ import me.vripper.gui.controller.PostController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.LoggerDelegate
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class ActionBarView : View() {
private val logger by LoggerDelegate()
private val downloadActiveProperty = SimpleBooleanProperty(true)
private val postController: PostController by inject()
private val postsTableView: PostsTableView by inject()
@@ -45,8 +44,7 @@ class ActionBarView : View() {
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.actionBar.forEach { it.cancelAndJoin() }
ActiveUICoroutines.actionBar.clear()
ActiveUICoroutines.cancelActionBar()
}
}
}
@@ -132,13 +130,20 @@ class ActionBarView : View() {
private fun connect(appEndpointService: IAppEndpointService) {
coroutineScope.launch {
appEndpointService.onQueueStateUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
ActiveUICoroutines.removeFromActionBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connect(appEndpointService)
}
}
}.collect {
runLater {
running.set(it.running)
}
}
}.also { ActiveUICoroutines.actionBar.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToActionBar(it) } }
}
}
@@ -11,6 +11,7 @@ import javafx.scene.control.cell.TextFieldTableCell
import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.entities.Status
@@ -62,8 +63,7 @@ class ImagesTableView : View("Photos") {
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.images.forEach { it.cancelAndJoin() }
ActiveUICoroutines.images.clear()
ActiveUICoroutines.cancelImages()
}
}
}
@@ -271,8 +271,7 @@ class ImagesTableView : View("Photos") {
}
fun setPostId(postId: Long?) {
ActiveUICoroutines.images.forEach { it.cancel() }
ActiveUICoroutines.images.clear()
runBlocking { ActiveUICoroutines.cancelImages() }
runLater {
items.clear()
}
@@ -290,7 +289,9 @@ class ImagesTableView : View("Photos") {
}
}
coroutineScope.launch {
imageController.onUpdateImages(postId).collect { image ->
imageController.onUpdateImages(postId).catch {
ActiveUICoroutines.removeFromImages(currentCoroutineContext().job)
}.collect { image ->
runLater {
val imageModel = items.find { it.id == image.id } ?: return@runLater
@@ -303,10 +304,12 @@ class ImagesTableView : View("Photos") {
)
}
}
}.also { ActiveUICoroutines.images.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToImages(it) } }
coroutineScope.launch {
imageController.onStopped().collect {
imageController.onStopped().catch {
ActiveUICoroutines.removeFromImages(currentCoroutineContext().job)
}.collect {
runLater {
items.forEach { imageModel ->
if (imageModel.status != Status.FINISHED.name) {
@@ -315,6 +318,6 @@ class ImagesTableView : View("Photos") {
}
}
}
}.also { ActiveUICoroutines.images.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToImages(it) } }
}
}
@@ -4,14 +4,11 @@ import javafx.scene.control.ProgressIndicator.INDETERMINATE_PROGRESS
import javafx.scene.effect.DropShadow
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filterIsInstance
import me.vripper.gui.VripperGuiApplication
import me.vripper.gui.components.fragments.SessionFragment
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.listener.GuiStartupLister
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.Watcher
import me.vripper.utilities.DatabaseManager
import me.vripper.listeners.AppManager
import tornadofx.*
class LoadingView : View("VRipper") {
@@ -26,42 +23,17 @@ class LoadingView : View("VRipper") {
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ApplicationInitialized::class).collect {
if (widgetsController.currentSettings.localSession) {
DatabaseManager.connect()
GuiStartupLister().run()
runLater {
replaceWith(find<AppView>())
}
AppManager.start()
} else {
grpcEndpointService.connect(
widgetsController.currentSettings.remoteSessionModel.host,
widgetsController.currentSettings.remoteSessionModel.port
widgetsController.currentSettings.remoteSessionModel.port,
widgetsController.currentSettings.remoteSessionModel.passcode,
)
repeat(10) {
if (grpcEndpointService.ready()) {
return@repeat
}
delay(333)
}
runLater {
replaceWith(find<AppView>())
runBlocking {
GuiEventBus.publishEvent(GuiEventBus.RemoteSessionFailure)
}
}
if (!grpcEndpointService.ready()) {
val sessionView = find<SessionFragment>()
runLater {
sessionView.openModal()?.apply {
setOnCloseRequest {
VripperGuiApplication.APP_INSTANCE.stop()
}
minWidth = 100.0
minHeight = 100.0
}
}
}
}
runLater {
replaceWith(find<AppView>())
}
if (it.args.isNotEmpty()) {
Watcher.notify(it.args[0])
}
@@ -2,10 +2,12 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import atlantafx.base.theme.Tweaks
import io.grpc.StatusException
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.control.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.fragments.LogMessageFragment
@@ -178,16 +180,8 @@ class LogTableView : View() {
connect()
}
is GuiEventBus.RemoteSessionFailure -> {
runLater {
items.clear()
tableView.placeholder = Label("Connection Failure")
}
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.logs.forEach { it.cancelAndJoin() }
ActiveUICoroutines.logs.clear()
ActiveUICoroutines.cancelLog()
runLater {
tableView.placeholder = Label("Loading")
items.clear()
@@ -202,8 +196,48 @@ class LogTableView : View() {
runBlocking {
maxLogEvent = logController.appEndpointService.getSettings().systemSettings.maxEventLog
}
connectToNewLogs()
connectToSettingsUpdate()
runLater {
runBlocking {
logController.initLogger()
}
}
}
private fun connectToSettingsUpdate() {
coroutineScope.launch {
logController.onNewLog().collect {
logController.onUpdateSettings().catch {
ActiveUICoroutines.removeFromLog(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToSettingsUpdate()
}
}
}.collect {
maxLogEvent = it.systemSettings.maxEventLog
}
}.also { runBlocking { ActiveUICoroutines.addToLog(it) } }
}
private fun connectToNewLogs() {
coroutineScope.launch {
logController.onNewLog().catch {
ActiveUICoroutines.removeFromLog(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToNewLogs()
}
}
}.collect {
runLater {
items.sortWith(Comparator.comparing { it.sequence })
while (items.isNotEmpty() && (items.size >= maxLogEvent)) {
@@ -213,17 +247,7 @@ class LogTableView : View() {
tableView.sort()
}
}
}.also { ActiveUICoroutines.logs.add(it) }
coroutineScope.launch {
logController.appEndpointService.onUpdateSettings().collect {
maxLogEvent = it.systemSettings.maxEventLog
}
}.also { ActiveUICoroutines.logs.add(it) }
runLater {
runBlocking {
logController.initLogger()
}
}
}.also { runBlocking { ActiveUICoroutines.addToLog(it) } }
}
private fun openLog(item: LogModel) {
@@ -1,5 +1,6 @@
package me.vripper.gui.components.views
import io.grpc.StatusException
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.scene.control.ButtonType
@@ -20,13 +21,11 @@ import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.gui.utils.openLink
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.ApplicationProperties
import me.vripper.utilities.LoggerDelegate
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class MenuBarView : View() {
private val logger by LoggerDelegate()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val downloadActiveProperty = SimpleBooleanProperty(false)
private val postsTableView: PostsTableView by inject()
@@ -54,8 +53,7 @@ class MenuBarView : View() {
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.menuBar.forEach { it.cancelAndJoin() }
ActiveUICoroutines.menuBar.clear()
ActiveUICoroutines.cancelMenuBar()
}
}
}
@@ -228,13 +226,20 @@ class MenuBarView : View() {
private fun connect(appEndpointService: IAppEndpointService) {
coroutineScope.launch {
appEndpointService.onQueueStateUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
ActiveUICoroutines.removeFromMenuBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connect(appEndpointService)
}
}
}.collect {
runLater {
running.set(it.running)
}
}
}.also { ActiveUICoroutines.menuBar.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToMenuBar(it) } }
}
}
@@ -10,13 +10,11 @@ import me.vripper.gui.controller.PostController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.PostModel
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.utilities.LoggerDelegate
import org.kordamp.ikonli.feather.Feather
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
class PostInfoView : View() {
private val logger by LoggerDelegate()
private val postController: PostController by inject()
private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val imagesTableView: ImagesTableView by inject()
@@ -29,8 +27,7 @@ class PostInfoView : View() {
init {
coroutineScope.launch {
GuiEventBus.events.filterIsInstance(GuiEventBus.ChangingSession::class).collect {
ActiveUICoroutines.postInfo.forEach { it.cancelAndJoin() }
ActiveUICoroutines.postInfo.clear()
ActiveUICoroutines.cancelPostInfo()
}
}
with(root) {
@@ -87,8 +84,7 @@ class PostInfoView : View() {
}
fun setPostId(postId: Long?) {
ActiveUICoroutines.postInfo.forEach { it.cancel() }
ActiveUICoroutines.postInfo.clear()
runBlocking { ActiveUICoroutines.cancelPostInfo() }
imagesTableView.setPostId(postId)
if (postId == null) {
postModel.apply {
@@ -145,8 +141,7 @@ class PostInfoView : View() {
}
coroutineScope.launch {
postController.onUpdatePosts().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
ActiveUICoroutines.removeFromPostInfo(currentCoroutineContext().job)
}.filter {
it.postId == postModel.postId
}.collect { post ->
@@ -164,12 +159,11 @@ class PostInfoView : View() {
postModel.folderName = post.folderName
}
}
}.also { ActiveUICoroutines.postInfo.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToPostInfo(it) } }
coroutineScope.launch {
postController.onUpdateMetadata().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
ActiveUICoroutines.removeFromPostInfo(currentCoroutineContext().job)
}.filter {
it.postId == postModel.postId
}.collect {
@@ -178,6 +172,6 @@ class PostInfoView : View() {
postModel.postedBy = it.data.postedBy
}
}
}.also { ActiveUICoroutines.postInfo.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToPostInfo(it) } }
}
}
@@ -2,6 +2,7 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import atlantafx.base.theme.Tweaks
import io.grpc.StatusException
import javafx.collections.FXCollections
import javafx.event.EventHandler
import javafx.geometry.Pos
@@ -12,6 +13,7 @@ import javafx.scene.input.KeyEvent
import javafx.scene.input.MouseButton
import javafx.util.Callback
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.Shared
@@ -25,6 +27,7 @@ import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.model.PostModel
import me.vripper.gui.services.ClipboardService
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.gui.utils.Preview
import me.vripper.gui.utils.openFileDirectory
@@ -36,14 +39,13 @@ import tornadofx.*
import kotlin.io.path.Path
class PostsTableView : View() {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val postController: PostController by inject()
private val widgetsController: WidgetsController by inject()
private val clipboardService: ClipboardService by inject()
private val mainView: MainView by inject()
private val localAppEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteAppEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val remoteAppEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
val tableView: TableView<PostModel>
var items: SortedFilteredList<PostModel> = SortedFilteredList()
@@ -52,7 +54,6 @@ class PostsTableView : View() {
override val root = vbox {}
init {
items.filterWhen(Shared.searchInput) { query, item ->
item.title.contains(query, ignoreCase = true)
|| item.postId.toString().contains(query)
@@ -74,16 +75,8 @@ class PostsTableView : View() {
connect()
}
is GuiEventBus.RemoteSessionFailure -> {
runLater {
items.clear()
tableView.placeholder = Label("Connection Failure")
}
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.posts.forEach { it.cancelAndJoin() }
ActiveUICoroutines.posts.clear()
ActiveUICoroutines.cancelPosts()
runLater {
tableView.placeholder = Label("Loading")
}
@@ -404,27 +397,70 @@ class PostsTableView : View() {
private fun connect() {
coroutineScope.launch {
val postModelList = async { postController.findAllPosts() }.await()
runLater {
items.clear()
items.addAll(postModelList)
tableView.sort()
tableView.placeholder = Label("No content in table")
clipboardService.init(postController.appEndpointService)
}
connectToOnNewPosts()
connectToOnPostUpdated()
connectToOnDeletedPost()
connectToOnMetadataUpdated()
}
}
private fun connectToOnMetadataUpdated() {
coroutineScope.launch {
postController.onNewPosts().collect {
postController.onUpdateMetadata().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnMetadataUpdated()
}
}
}.collect {
runLater {
items.addAll(it)
val postModel = items.find { it.postId == it.postId } ?: return@runLater
postModel.altTitles = FXCollections.observableArrayList(it.data.resolvedNames)
postModel.postedBy = it.data.postedBy
}
}
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun connectToOnDeletedPost() {
coroutineScope.launch {
postController.onDeletePosts().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnDeletedPost()
}
}
}.collect {
runLater {
items.items.removeIf { p -> p.postId == it }
tableView.sort()
}
}
}.also { ActiveUICoroutines.posts.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun connectToOnPostUpdated() {
coroutineScope.launch {
postController.onUpdatePosts().collect { post ->
postController.onUpdatePosts().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnPostUpdated()
}
}
}.collect { post ->
runLater {
val postModel = items.find { it.postId == post.postId } ?: return@runLater
@@ -441,27 +477,38 @@ class PostsTableView : View() {
postModel.folderName = post.folderName
}
}
}.also { ActiveUICoroutines.posts.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun connectToOnNewPosts() {
coroutineScope.launch {
postController.onDeletePosts().collect {
val postModelList = async { postController.findAllPosts() }.await()
runLater {
items.clear()
items.addAll(postModelList)
tableView.sort()
tableView.placeholder = Label("No content in table")
clipboardService.init(postController.appEndpointService)
}
}
coroutineScope.launch {
postController.onNewPosts().catch {
ActiveUICoroutines.removeFromPosts(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToOnNewPosts()
}
}
}.collect {
runLater {
items.items.removeIf { p -> p.postId == it }
items.addAll(it)
tableView.sort()
}
}
}.also { ActiveUICoroutines.posts.add(it) }
coroutineScope.launch {
postController.onUpdateMetadata().collect {
runLater {
val postModel = items.find { it.postId == it.postId } ?: return@runLater
postModel.altTitles = FXCollections.observableArrayList(it.data.resolvedNames)
postModel.postedBy = it.data.postedBy
}
}
}.also { ActiveUICoroutines.posts.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToPosts(it) } }
}
private fun rename(post: PostModel) {
@@ -477,13 +524,6 @@ class PostsTableView : View() {
}
}
fun renameSelected() {
val selectedItem = tableView.selectionModel.selectedItem
if (selectedItem != null) {
rename(selectedItem)
}
}
fun bulkRenameSelected() {
val selectedItems = tableView.selectionModel.selectedItems
coroutineScope.launch {
@@ -1,5 +1,7 @@
package me.vripper.gui.components.views
import io.grpc.ConnectivityState
import io.grpc.StatusException
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
@@ -9,17 +11,16 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import me.vripper.gui.controller.WidgetsController
import me.vripper.gui.event.GuiEventBus
import me.vripper.gui.services.GrpcEndpointService
import me.vripper.gui.utils.ActiveUICoroutines
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.formatSI
import tornadofx.*
class StatusBarView : View("Status bar") {
private val logger by LoggerDelegate()
private val widgetsController: WidgetsController by inject()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val grpcEndpointService: IAppEndpointService by di("remoteAppEndpointService")
private val grpcEndpointService: GrpcEndpointService by di("remoteAppEndpointService")
private val localEndpointService: IAppEndpointService by di("localAppEndpointService")
private val remoteText = SimpleStringProperty()
private val loggedUser = SimpleStringProperty()
@@ -49,15 +50,8 @@ class StatusBarView : View("Status bar") {
connect(grpcEndpointService)
}
is GuiEventBus.RemoteSessionFailure -> {
runLater {
remoteText.set("Unable to connect to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}")
}
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.statusBar.forEach { it.cancelAndJoin() }
ActiveUICoroutines.statusBar.clear()
ActiveUICoroutines.cancelStatusBar()
runLater {
remoteText.set("Connecting to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}")
}
@@ -75,81 +69,136 @@ class StatusBarView : View("Status bar") {
}
}
coroutineScope.launch {
endpointService.onVGUserUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
loggedUser.set(it)
}
}
}.also { ActiveUICoroutines.statusBar.add(it) }
connectToVGUserUpdate(endpointService)
connectToTasksRunning(endpointService)
connectToDownloadSpeed(endpointService)
connectToQueueStateUpdate(endpointService)
connectToErrorCountUpdate(endpointService)
coroutineScope.launch {
endpointService.onTasksRunning().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
tasksRunning.set(it)
}
}
}.also { ActiveUICoroutines.statusBar.add(it) }
coroutineScope.launch {
endpointService.onDownloadSpeed().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
downloadSpeed.set(it.speed.formatSI())
}
}
}.also { ActiveUICoroutines.statusBar.add(it) }
coroutineScope.launch {
endpointService.onQueueStateUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
running.set(it.running)
pending.set(it.remaining)
}
}
}.also { ActiveUICoroutines.statusBar.add(it) }
coroutineScope.launch {
endpointService.onErrorCountUpdate().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.collect {
runLater {
error.set(it.count)
}
}
}.also { ActiveUICoroutines.statusBar.add(it) }
if (widgetsController.currentSettings.localSession) {
runLater {
remoteText.set("")
}
} else {
coroutineScope.launch {
if (grpcEndpointService.ready()) {
val version = grpcEndpointService.getVersion()
runLater {
remoteText.set("Connected to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port} v$version")
while (isActive) {
val text = when (grpcEndpointService.connectionState()) {
ConnectivityState.CONNECTING -> "Connecting to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
ConnectivityState.READY -> "Connected to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port} ${grpcEndpointService.getVersion()}"
ConnectivityState.TRANSIENT_FAILURE -> "Failing to connect to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
ConnectivityState.IDLE -> "Idle connection to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
ConnectivityState.SHUTDOWN -> "Connection shutdown to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}"
}
} else {
runLater {
remoteText.set("Unable to connect to ${widgetsController.currentSettings.remoteSessionModel.host}:${widgetsController.currentSettings.remoteSessionModel.port}")
remoteText.set(text)
}
delay(1000)
}
}
}
}
private fun connectToErrorCountUpdate(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onErrorCountUpdate().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToErrorCountUpdate(endpointService)
}
}
}.collect {
runLater {
error.set(it.count)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
private fun connectToQueueStateUpdate(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onQueueStateUpdate().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToQueueStateUpdate(endpointService)
}
}
}.collect {
runLater {
running.set(it.running)
pending.set(it.remaining)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
private fun connectToDownloadSpeed(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onDownloadSpeed().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToDownloadSpeed(endpointService)
}
}
}.collect {
runLater {
downloadSpeed.set(it.speed.formatSI())
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
private fun connectToTasksRunning(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onTasksRunning().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToTasksRunning(endpointService)
}
}
}.collect {
runLater {
tasksRunning.set(it)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
private fun connectToVGUserUpdate(endpointService: IAppEndpointService) {
coroutineScope.launch {
endpointService.onVGUserUpdate().catch {
ActiveUICoroutines.removeFromStatusBar(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToVGUserUpdate(endpointService)
}
}
}.collect {
runLater {
loggedUser.set(it)
}
}
}.also { runBlocking { ActiveUICoroutines.addToStatusBar(it) } }
}
override val root = borderpane {
id = "statusbar"
left {
@@ -2,12 +2,14 @@ package me.vripper.gui.components.views
import atlantafx.base.theme.Styles
import atlantafx.base.theme.Tweaks
import io.grpc.StatusException
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.control.*
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyEvent
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.javafx.asFlow
import me.vripper.gui.components.fragments.ThreadSelectionTableFragment
@@ -50,8 +52,7 @@ class ThreadTableView : View() {
}
is GuiEventBus.ChangingSession -> {
ActiveUICoroutines.threads.forEach { it.cancelAndJoin() }
ActiveUICoroutines.threads.clear()
ActiveUICoroutines.cancelThreads()
runLater {
tableView.placeholder = Label("Loading")
}
@@ -182,6 +183,75 @@ class ThreadTableView : View() {
}
fun connect() {
connectToNewThread()
connectToUpdateThread()
connectToDeleteThread()
connectToClearThread()
}
private fun connectToClearThread() {
coroutineScope.launch {
threadController.onClearThreads().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToClearThread()
}
}
}.collect {
runLater {
tableView.items.clear()
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun connectToDeleteThread() {
coroutineScope.launch {
threadController.onDeleteThread().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToDeleteThread()
}
}
}.collect { threadId ->
runLater {
tableView.items.removeIf { it.threadId == threadId }
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun connectToUpdateThread() {
coroutineScope.launch {
threadController.onUpdateThread().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToUpdateThread()
}
}
}.collect { thread ->
runLater {
val threadModel = items.find { it.threadId == thread.threadId } ?: return@runLater
threadModel.total = thread.total
threadModel.title = thread.title
}
}
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun connectToNewThread() {
coroutineScope.launch {
val list = async {
threadController.findAll()
@@ -193,38 +263,22 @@ class ThreadTableView : View() {
}
}
coroutineScope.launch {
threadController.onNewThread().collect {
threadController.onNewThread().catch {
ActiveUICoroutines.removeFromThreads(currentCoroutineContext().job)
if (it is StatusException) {
//reconnect
coroutineScope.launch {
delay(1000)
connectToNewThread()
}
}
}.collect {
runLater {
items.add(it)
}
}
}.also { ActiveUICoroutines.threads.add(it) }
coroutineScope.launch {
threadController.onUpdateThread().collect { thread ->
runLater {
val threadModel = items.find { it.threadId == thread.threadId } ?: return@runLater
threadModel.total = thread.total
threadModel.title = thread.title
}
}
}.also { ActiveUICoroutines.threads.add(it) }
coroutineScope.launch {
threadController.onDeleteThread().collect { threadId ->
runLater {
tableView.items.removeIf { it.threadId == threadId }
}
}
}.also { ActiveUICoroutines.threads.add(it) }
coroutineScope.launch {
threadController.onClearThreads().collect {
runLater {
tableView.items.clear()
}
}
}.also { ActiveUICoroutines.threads.add(it) }
}.also { runBlocking { ActiveUICoroutines.addToThreads(it) } }
}
private fun isCurrentTab(): Boolean = mainView.root.selectionModel.selectedItem.id == "thread-tab"
@@ -1,16 +1,11 @@
package me.vripper.gui.controller
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.catch
import me.vripper.gui.model.ImageModel
import me.vripper.model.Image
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.LoggerDelegate
import tornadofx.Controller
class ImageController : Controller() {
private val logger by LoggerDelegate()
lateinit var appEndpointService: IAppEndpointService
suspend fun findImages(postId: Long): List<ImageModel> {
return appEndpointService.findImagesByPostId(postId).map(::mapper)
@@ -39,14 +34,8 @@ class ImageController : Controller() {
}
fun onUpdateImages(postId: Long) =
appEndpointService.onUpdateImagesByPostId(postId).catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
appEndpointService.onUpdateImagesByPostId(postId)
fun onStopped() = appEndpointService.onStopped().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
fun onStopped() = appEndpointService.onStopped()
}
@@ -1,17 +1,12 @@
package me.vripper.gui.controller
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import me.vripper.gui.model.LogModel
import me.vripper.model.LogEntry
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.LoggerDelegate
import tornadofx.Controller
class LogController : Controller() {
private val logger by LoggerDelegate()
lateinit var appEndpointService: IAppEndpointService
private fun mapper(it: LogEntry): LogModel {
@@ -26,12 +21,13 @@ class LogController : Controller() {
)
}
fun onNewLog() = appEndpointService.onNewLog().map(::mapper).catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
fun onNewLog() = appEndpointService.onNewLog().map(::mapper)
suspend fun initLogger() {
appEndpointService.initLogger()
}
fun onUpdateSettings() =
appEndpointService.onUpdateSettings()
}
@@ -1,20 +1,15 @@
package me.vripper.gui.controller
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import me.vripper.gui.model.PostModel
import me.vripper.model.Post
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.LoggerDelegate
import me.vripper.utilities.formatSI
import tornadofx.Controller
import java.time.format.DateTimeFormatter
class PostController : Controller() {
private val logger by LoggerDelegate()
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")
lateinit var appEndpointService: IAppEndpointService
@@ -94,29 +89,16 @@ class PostController : Controller() {
}
fun onNewPosts() =
appEndpointService.onNewPosts().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}.map { post ->
appEndpointService.onNewPosts().map { post ->
mapper(post)
}
fun onUpdatePosts() =
appEndpointService.onUpdatePosts().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
appEndpointService.onUpdatePosts()
fun onDeletePosts() =
appEndpointService.onDeletePosts().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
appEndpointService.onDeletePosts()
fun onUpdateMetadata() =
appEndpointService.onUpdateMetadata().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
appEndpointService.onUpdateMetadata()
}
@@ -1,20 +1,15 @@
package me.vripper.gui.controller
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import me.vripper.entities.ThreadEntity
import me.vripper.gui.model.ThreadModel
import me.vripper.gui.model.ThreadSelectionModel
import me.vripper.model.ThreadPostId
import me.vripper.services.IAppEndpointService
import me.vripper.utilities.LoggerDelegate
import org.koin.core.component.KoinComponent
import tornadofx.Controller
class ThreadController : KoinComponent, Controller() {
private val logger by LoggerDelegate()
lateinit var appEndpointService: IAppEndpointService
suspend fun findAll(): List<ThreadModel> {
@@ -61,23 +56,11 @@ class ThreadController : KoinComponent, Controller() {
})
}
fun onNewThread() = appEndpointService.onNewThread().map(::threadModelMapper).catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
fun onNewThread() = appEndpointService.onNewThread().map(::threadModelMapper)
fun onUpdateThread() = appEndpointService.onUpdateThread().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
fun onUpdateThread() = appEndpointService.onUpdateThread()
fun onDeleteThread() = appEndpointService.onDeleteThread().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
fun onDeleteThread() = appEndpointService.onDeleteThread()
fun onClearThreads() = appEndpointService.onClearThreads().catch {
logger.error("gRPC error", it)
currentCoroutineContext().cancel(null)
}
fun onClearThreads() = appEndpointService.onClearThreads()
}
@@ -235,6 +235,9 @@ class WidgetsController : Controller() {
currentSettings.remoteSessionModel.portProperty.onChange {
WidgetSettings.update(currentSettings)
}
currentSettings.remoteSessionModel.passcodeProperty.onChange {
WidgetSettings.update(currentSettings)
}
currentSettings.cachePathProperty.onChange {
WidgetSettings.update(currentSettings)
}
@@ -16,5 +16,4 @@ object GuiEventBus {
object ChangingSession
object LocalSession
object RemoteSession
object RemoteSessionFailure
}
@@ -1,10 +0,0 @@
package me.vripper.gui.listener
import me.vripper.listeners.OnStartupListener
class GuiStartupLister : OnStartupListener() {
override fun run() {
super.run()
}
}
@@ -8,10 +8,14 @@ import tornadofx.setValue
class RemoteSessionModel(
host: String,
port: Int,
passcode: String,
) {
val hostProperty = SimpleStringProperty(host)
var host: String by hostProperty
val portProperty = SimpleIntegerProperty(port)
var port: Int by portProperty
val passcodeProperty = SimpleStringProperty(passcode)
var passcode: String by passcodeProperty
}
@@ -159,6 +159,7 @@ class WidgetsViewModel(
val remoteSessionModel = RemoteSessionModel(
remoteSession.host,
remoteSession.port
remoteSession.port,
remoteSession.passPhrase,
)
}
@@ -0,0 +1,22 @@
package me.vripper.gui.services
import io.grpc.*
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall
class ClientE2eEncryptingInterceptor(private val passPhrase: String) : ClientInterceptor {
override fun <ReqT, RespT> interceptCall(
method: MethodDescriptor<ReqT?, RespT?>,
callOptions: CallOptions?, next: Channel
): ClientCall<ReqT, RespT> {
return object : SimpleForwardingClientCall<ReqT, RespT>(
next.newCall(
method.toBuilder(
ClientRequestEncryptor(method.getRequestMarshaller(), passPhrase),
ClientResponseDecryptor(method.getResponseMarshaller(), passPhrase)
).build(),
callOptions
)
) {}
}
}
@@ -0,0 +1,22 @@
package me.vripper.gui.services
import io.grpc.MethodDescriptor
import me.vripper.utilities.AesUtils
import java.io.ByteArrayInputStream
import java.io.InputStream
class ClientRequestEncryptor<T>(
private val protosMarshaller: MethodDescriptor.Marshaller<T>,
private val passPhrase: String
) :
MethodDescriptor.Marshaller<T> {
override fun stream(value: T): InputStream {
return ByteArrayInputStream(AesUtils.aesEncrypt(protosMarshaller.stream(value).readBytes(), passPhrase))
}
override fun parse(stream: InputStream): T {
return protosMarshaller.parse(stream)
}
}
@@ -0,0 +1,22 @@
package me.vripper.gui.services
import io.grpc.MethodDescriptor
import me.vripper.utilities.AesUtils
import java.io.ByteArrayInputStream
import java.io.InputStream
class ClientResponseDecryptor<T>(
private val protoMarshaller: MethodDescriptor.Marshaller<T>,
private val passPhrase: String
) :
MethodDescriptor.Marshaller<T> {
override fun stream(value: T): InputStream {
return protoMarshaller.stream(value)
}
override fun parse(encryptedStream: InputStream): T {
return protoMarshaller.parse(ByteArrayInputStream(AesUtils.aesDecrypt(encryptedStream.readBytes(), passPhrase)))
}
}
@@ -1,5 +1,6 @@
package me.vripper.gui.services
import io.grpc.StatusException
import javafx.scene.input.Clipboard
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.catch
@@ -21,7 +22,11 @@ class ClipboardService : Controller() {
fun init(appEndpointService: IAppEndpointService) {
subscribeJob?.cancel()
subscribeJob = coroutineScope.launch {
appEndpointService.onUpdateSettings().catch { logger.error("gRPC error", it) }.collect {
appEndpointService.onUpdateSettings().catch {
if (it !is StatusException) {
logger.error("gRPC error", it)
}
}.collect {
run(it, appEndpointService)
}
}
@@ -20,7 +20,7 @@ import me.vripper.services.IAppEndpointService
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class GrpcEndpointService : IAppEndpointService {
internal class GrpcEndpointService : IAppEndpointService {
private var channel: ManagedChannel? = null
private var endpointServiceCoroutineStub: EndpointServiceGrpcKt.EndpointServiceCoroutineStub? = null
@@ -356,8 +356,14 @@ class GrpcEndpointService : IAppEndpointService {
)
}
fun connect(host: String, port: Int) {
channel = ManagedChannelBuilder.forAddress(host, port).maxInboundMessageSize(134217728).usePlaintext().build()
fun connect(host: String, port: Int, passPhrase: String) {
disconnect()
channel = ManagedChannelBuilder
.forAddress(host, port)
.maxInboundMessageSize(Integer.MAX_VALUE)
.usePlaintext()
.intercept(ClientE2eEncryptingInterceptor(passPhrase))
.build()
endpointServiceCoroutineStub = EndpointServiceGrpcKt.EndpointServiceCoroutineStub(channel!!)
}
@@ -369,9 +375,5 @@ class GrpcEndpointService : IAppEndpointService {
}
}
fun connectionState(): ConnectivityState = channel?.getState(true) ?: ConnectivityState.SHUTDOWN
override fun ready(): Boolean {
return connectionState() == ConnectivityState.READY
}
}
fun connectionState(): ConnectivityState = channel?.getState(false) ?: ConnectivityState.SHUTDOWN
}
@@ -1,25 +1,192 @@
package me.vripper.gui.utils
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
object ActiveUICoroutines {
val posts: MutableList<Job> = mutableListOf()
val actionBar: MutableList<Job> = mutableListOf()
val images: MutableList<Job> = mutableListOf()
val logs: MutableList<Job> = mutableListOf()
val menuBar: MutableList<Job> = mutableListOf()
val postInfo: MutableList<Job> = mutableListOf()
val statusBar: MutableList<Job> = mutableListOf()
val threads: MutableList<Job> = mutableListOf()
fun all() = listOf(
posts,
actionBar,
images,
logs,
menuBar,
postInfo,
statusBar,
threads
).flatten()
private val mutex = Mutex()
private val posts: MutableList<Job> = mutableListOf()
private val actionBar: MutableList<Job> = mutableListOf()
private val images: MutableList<Job> = mutableListOf()
private val logs: MutableList<Job> = mutableListOf()
private val menuBar: MutableList<Job> = mutableListOf()
private val postInfo: MutableList<Job> = mutableListOf()
private val statusBar: MutableList<Job> = mutableListOf()
private val threads: MutableList<Job> = mutableListOf()
suspend fun cancelPosts() {
mutex.withLock {
posts.forEach { it.cancelAndJoin() }
posts.clear()
}
}
suspend fun addToPosts(job: Job) {
mutex.withLock {
posts.add(job)
}
}
suspend fun removeFromPosts(job: Job) {
mutex.withLock {
job.cancel()
posts.remove(job)
}
}
suspend fun cancelActionBar() {
mutex.withLock {
actionBar.forEach { it.cancelAndJoin() }
actionBar.clear()
}
}
suspend fun addToActionBar(job: Job) {
mutex.withLock {
actionBar.add(job)
}
}
suspend fun removeFromActionBar(job: Job) {
mutex.withLock {
job.cancel()
actionBar.remove(job)
}
}
suspend fun cancelImages() {
mutex.withLock {
images.forEach { it.cancelAndJoin() }
images.clear()
}
}
suspend fun addToImages(job: Job) {
mutex.withLock {
images.add(job)
}
}
suspend fun removeFromImages(job: Job) {
mutex.withLock {
job.cancel()
images.remove(job)
}
}
suspend fun cancelLog() {
mutex.withLock {
logs.forEach { it.cancelAndJoin() }
logs.clear()
}
}
suspend fun addToLog(job: Job) {
mutex.withLock {
logs.add(job)
}
}
suspend fun removeFromLog(job: Job) {
mutex.withLock {
job.cancel()
logs.remove(job)
}
}
suspend fun cancelMenuBar() {
mutex.withLock {
menuBar.forEach { it.cancelAndJoin() }
menuBar.clear()
}
}
suspend fun addToMenuBar(job: Job) {
mutex.withLock {
menuBar.add(job)
}
}
suspend fun removeFromMenuBar(job: Job) {
mutex.withLock {
job.cancel()
menuBar.remove(job)
}
}
suspend fun cancelPostInfo() {
mutex.withLock {
postInfo.forEach { it.cancelAndJoin() }
postInfo.clear()
}
}
suspend fun addToPostInfo(job: Job) {
mutex.withLock {
postInfo.add(job)
}
}
suspend fun removeFromPostInfo(job: Job) {
mutex.withLock {
job.cancel()
postInfo.remove(job)
}
}
suspend fun cancelStatusBar() {
mutex.withLock {
statusBar.forEach { it.cancelAndJoin() }
statusBar.clear()
}
}
suspend fun addToStatusBar(job: Job) {
mutex.withLock {
statusBar.add(job)
}
}
suspend fun removeFromStatusBar(job: Job) {
mutex.withLock {
job.cancel()
statusBar.remove(job)
}
}
suspend fun cancelThreads() {
mutex.withLock {
threads.forEach { it.cancelAndJoin() }
threads.clear()
}
}
suspend fun addToThreads(job: Job) {
mutex.withLock {
threads.add(job)
}
}
suspend fun removeFromThreads(job: Job) {
mutex.withLock {
job.cancel()
threads.remove(job)
}
}
suspend fun all() = mutex.withLock {
listOf(
posts,
actionBar,
logs,
menuBar,
postInfo,
statusBar,
threads
).flatten()
}
}
@@ -123,6 +123,7 @@ object WidgetSettings {
data class RemoteSession(
val host: String = "",
val port: Int = 30000,
val passPhrase: String = "",
)
@Serializable
@@ -212,7 +213,8 @@ object WidgetSettings {
),
RemoteSession(
currentSettings.remoteSessionModel.host,
currentSettings.remoteSessionModel.port
currentSettings.remoteSessionModel.port,
currentSettings.remoteSessionModel.passcode
),
PostsTableColumnsWidth(
currentSettings.postsColumnsWidthModel.preview,
@@ -1,7 +1,6 @@
package me.vripper
import me.vripper.listeners.OnStartupListener
import me.vripper.utilities.DatabaseManager
import me.vripper.listeners.AppManager
import org.koin.core.context.startKoin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.builder.SpringApplicationBuilder
@@ -10,10 +9,9 @@ import org.springframework.boot.builder.SpringApplicationBuilder
class VripperWebApplication
fun main(args: Array<String>) {
DatabaseManager.connect()
startKoin {
modules(coreModule)
}
OnStartupListener().run()
AppManager.start()
SpringApplicationBuilder(VripperWebApplication::class.java).listeners(AppListener()).run(*args)
}
@@ -2,23 +2,37 @@ package me.vripper.web.grpc
import io.grpc.Server
import io.grpc.ServerBuilder
import io.grpc.ServerInterceptors
import me.vripper.utilities.LoggerDelegate
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
@Component
class GrpcServer(
@Value("\${grpc.enabled}") private val enabled: Boolean,
@Value("\${grpc.port}") private val port: Int,
@Value("\${grpc.passphrase}") private val passPhrase: String,
grpcServerAppEndpointService: GrpcServerAppEndpointService
) {
private val log by LoggerDelegate()
private val server: Server =
ServerBuilder.forPort(port).addService(grpcServerAppEndpointService).build()
ServerBuilder
.forPort(port)
.addService(
ServerInterceptors.intercept(
ServerInterceptors.useMarshalledMessages(
grpcServerAppEndpointService.bindService(),
ServerRequestDecryptor(passPhrase),
ServerResponseEncryptor(passPhrase)
)
)
)
.build()
@PostConstruct
fun start() {
@@ -29,4 +43,9 @@ class GrpcServer(
log.info("gRPC is disabled, remote connection to this instance is not possible")
}
}
@PreDestroy
fun destroy() {
server.shutdown()
}
}
@@ -0,0 +1,18 @@
package me.vripper.web.grpc
import io.grpc.MethodDescriptor
import me.vripper.utilities.AesUtils
import java.io.ByteArrayInputStream
import java.io.InputStream
class ServerRequestDecryptor(private val passPhrase: String) : MethodDescriptor.Marshaller<InputStream> {
override fun stream(encryptedStream: InputStream): InputStream {
return ByteArrayInputStream(AesUtils.aesDecrypt(encryptedStream.readBytes(), passPhrase))
}
override fun parse(stream: InputStream?): InputStream? {
return stream
}
}
@@ -0,0 +1,18 @@
package me.vripper.web.grpc
import io.grpc.MethodDescriptor
import me.vripper.utilities.AesUtils
import java.io.ByteArrayInputStream
import java.io.InputStream
class ServerResponseEncryptor(private val passPhrase: String) : MethodDescriptor.Marshaller<InputStream> {
override fun stream(serializedProto: InputStream): InputStream {
return ByteArrayInputStream(AesUtils.aesEncrypt(serializedProto.readBytes(), passPhrase))
}
override fun parse(stream: InputStream?): InputStream? {
return stream
}
}