Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import io.github.freya022.botcommands.api.core.config.BConfigBuilder
import io.github.freya022.botcommands.api.core.service.annotations.BService
import io.github.freya022.botcommands.api.core.service.annotations.InterfacedService
import io.github.freya022.botcommands.api.core.service.getService
import io.github.freya022.botcommands.internal.core.events.BApplicationStartEvent
import io.github.freya022.botcommands.internal.core.hooks.ApplicationStartListener
import io.github.freya022.botcommands.internal.core.service.BCBotCommandsBootstrap
import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.*
import kotlin.time.DurationUnit
import kotlin.time.measureTimedValue

Expand Down Expand Up @@ -60,6 +63,10 @@ object BotCommands {
}

private fun build(config: BConfig): BContext {
val startEvent = BApplicationStartEvent(config)
val startListeners = ServiceLoader.load(ApplicationStartListener::class.java)
startListeners.forEach { it.onApplicationStart(startEvent) }

val (context, duration) = measureTimedValue {
val bootstrap = BCBotCommandsBootstrap(config)
bootstrap.injectAndLoadServices()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.freya022.botcommands.internal.core.events

import io.github.freya022.botcommands.api.core.config.BConfig

class BApplicationStartEvent internal constructor(val config: BConfig)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.github.freya022.botcommands.internal.core.hooks

import io.github.freya022.botcommands.internal.core.events.BApplicationStartEvent

interface ApplicationStartListener {

fun onApplicationStart(event: BApplicationStartEvent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import kotlin.reflect.KFunction

internal object MethodAccessorFactoryProvider {

private lateinit var accessorFactory: MethodAccessorFactory
private lateinit var accessorFactory: CachingMethodAccessorFactory
private val staticAccessors: MutableMap<KFunction<*>, MethodAccessor<*>> = hashMapOf()

internal fun getAccessorFactory(): MethodAccessorFactory {
Expand All @@ -26,6 +26,12 @@ internal object MethodAccessorFactoryProvider {
return accessorFactory
}

internal fun clearCache() {
if (::accessorFactory.isInitialized) {
accessorFactory.clearCache()
}
}

@OptIn(ExperimentalMethodAccessorsApi::class)
private fun loadAccessorFactory(): MethodAccessorFactory {
if (MethodAccessorsConfig.preferClassFileAccessors) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import io.github.freya022.botcommands.api.core.events.PreLoadEvent
import io.github.freya022.botcommands.api.core.objectLogger
import io.github.freya022.botcommands.api.core.service.getService
import io.github.freya022.botcommands.internal.core.BContextImpl
import io.github.freya022.botcommands.internal.core.method.accessors.MethodAccessorFactoryProvider
import io.github.freya022.botcommands.internal.emojis.AppEmojisLoader
import io.github.freya022.botcommands.internal.utils.ReflectionMetadata
import kotlinx.coroutines.runBlocking
import kotlin.time.DurationUnit
Expand All @@ -18,6 +20,9 @@ abstract class AbstractBotCommandsBootstrap(protected val config: BConfig) : Bot
protected val logger = objectLogger()

protected fun init() {
MethodAccessorFactoryProvider.clearCache()
AppEmojisLoader.clear()

measure("Scanned reflection metadata") {
ReflectionMetadata.runScan(config, this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import io.github.freya022.botcommands.api.core.service.ClassGraphProcessor
import io.github.freya022.botcommands.api.core.service.ServiceContainer
import io.github.freya022.botcommands.api.core.utils.findAnnotationRecursive
import io.github.freya022.botcommands.api.emojis.annotations.AppEmojiContainer
import org.jetbrains.annotations.TestOnly
import kotlin.reflect.KClass

internal object AppEmojiContainerProcessor : ClassGraphProcessor {
Expand All @@ -23,8 +22,7 @@ internal object AppEmojiContainerProcessor : ClassGraphProcessor {
}
}

@TestOnly
internal fun clear() {
emojiClasses.clear()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.entities.Icon
import net.dv8tion.jda.api.entities.emoji.ApplicationEmoji
import net.dv8tion.jda.internal.utils.Checks
import org.jetbrains.annotations.TestOnly
import kotlin.math.abs
import kotlin.reflect.KProperty
import kotlin.reflect.full.declaredMemberProperties
Expand Down Expand Up @@ -235,7 +234,6 @@ internal class AppEmojisLoader internal constructor(
private val toLoad = arrayListOf<LoadRequest>()
private val loadedEmojis = hashMapOf<String, ApplicationEmoji>()

@TestOnly
internal fun clear() {
loaded = false
toLoadEmojiNames.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class CachingMethodAccessorFactory(private val delegate: MethodAccessorFactory)
private val cache = WeakHashMap<Executable, MethodAccessor<*>>()
private val lock = ReentrantLock()

fun clearCache() {
cache.clear()
}

override fun <R> create(
instance: Any?,
function: KFunction<R>,
Expand Down
73 changes: 73 additions & 0 deletions BotCommands-restarter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
[bc-module-maven-central-shield]: https://img.shields.io/maven-central/v/io.github.freya022/BotCommands-restarter?label=Maven%20central&logo=apachemaven&versionPrefix=3
[bc-module-maven-central-link]: https://central.sonatype.com/artifact/io.github.freya022/BotCommands-restarter

# BotCommands module - Hot restarter
This module enables fast restarts of your bot as you develop it.

When you build changes of your code, it restarts automatically, in the same JVM,
leading to much faster restarts, as it doesn't need to recompile most of the code.

> [!WARNING]
> If you are using Spring, use [`spring-boot-devtools`](https://docs.spring.io/spring-boot/reference/using/devtools.html) instead.

## Installing
[![BotCommands-restarter on maven central][bc-module-maven-central-shield] ][bc-module-maven-central-link]

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add maven central badge

### Maven
```xml
<dependencies>
<dependency>
<groupId>io.github.freya022</groupId>
<artifactId>BotCommands-restarter</artifactId>
<version>VERSION</version>
</dependency>
</dependencies>
```

### Gradle
```gradle
repositories {
mavenCentral()
}

dependencies {
implementation("io.github.freya022:BotCommands-restarter:VERSION")
}
```

### Snapshots

To use the latest, unreleased changes, see [SNAPSHOTS.md](../SNAPSHOTS.md).

## Usage
You can enable the feature by doing so, after which, every build will restart your application.

### Kotlin
```kotlin
fun main(args: Array<out String>) {
// ...
BotCommands.create {
// ...

@OptIn(ExperimentalRestartApi::class)
registerRestarter(args) {
// Optional configuration
}
}
}
```

### Java
```java
void main(String[] args) {
// ...
BotCommands.create(config -> {
// ...

var restarterConfig = RestarterConfig.builder(args)
// Optional configuration
.build();
config.registerModule(restarterConfig);
});
}
```
31 changes: 31 additions & 0 deletions BotCommands-restarter/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import dev.freya02.botcommands.plugins.configureJarArtifact

plugins {
id("repositories-conventions")
id("kotlin-conventions")
id("publish-conventions")
id("dokka-conventions")
}

dependencies {
api(projects.botCommandsCore)

// Logging
implementation(libs.kotlin.logging)
}

kotlin {
compilerOptions {
freeCompilerArgs.addAll(
"-opt-in=dev.freya02.botcommands.restarter.api.ExperimentalRestartApi",
)
}
}

publishedProjectEnvironment {
configureJarArtifact(
artifactId = "BotCommands-restarter",
description = "Enables restarting your bot on the same JVM during development.",
url = "https://github.com/freya022/BotCommands/tree/3.X/BotCommands-restarter",
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.freya02.botcommands.restarter.api

/**
* Opt-in marker annotation for the hot restart feature.
*
* This feature provides no guarantee and its API may change (including removals) at any time.
*
* Please create an issue if you encounter a problem, including if it needs adaptations for your use case.
*/
@RequiresOptIn(
message = "This feature is experimental, please see the documentation of this opt-in annotation (@ExperimentalRestartApi) for more details.",
level = RequiresOptIn.Level.ERROR
)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
annotation class ExperimentalRestartApi
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package dev.freya02.botcommands.restarter.api.config

import dev.freya02.botcommands.restarter.api.ExperimentalRestartApi
import dev.freya02.botcommands.restarter.internal.exceptions.throwInternal
import io.github.freya022.botcommands.api.core.config.BConfig
import io.github.freya022.botcommands.api.core.config.BConfigBuilder
import io.github.freya022.botcommands.api.core.config.IConfig
import io.github.freya022.botcommands.api.core.config.getConfigOrNull
import io.github.freya022.botcommands.api.core.service.annotations.InjectedService
import io.github.freya022.botcommands.internal.core.config.ConfigDSL
import java.time.Duration as JavaDuration
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
import kotlin.time.toKotlinDuration

@ExperimentalRestartApi
interface RestarterConfigProps {

/**
* The program arguments passed to the main function upon restarting.
*/
val startArgs: List<String>

/**
* The time to wait before assuming all changes were compiled,
* so the application can be restarted with the new changes.
*
* Default: 1 second
*/
val restartDelay: Duration

/**
* Returns the time to wait before assuming all changes were compiled,
* so the application can be restarted with the new changes.
*
* Default: 1 second
*/
fun getRestartDelay(): JavaDuration = restartDelay.toJavaDuration()
}

/**
* Configuration for the restarter feature.
*
* To enable this feature, a configuration of it must be registered.
*
* @see [RestarterConfig.builder]
* @see [registerRestarter]
*/
@InjectedService
@ExperimentalRestartApi
interface RestarterConfig : IConfig, RestarterConfigProps {

override val configType get() = RestarterConfig::class.java

companion object {
/**
* Creates a new [RestarterConfigBuilder], you must [build] it and [register][BConfigBuilder.registerModule] it.
*
* @param args The program arguments, they will be passed to the main method upon restarting
*/
fun builder(args: Array<out String>): RestarterConfigBuilder {
return RestarterConfigBuilder.create(args)
}
}
}

@ExperimentalRestartApi
internal val BConfig.restarterConfig: RestarterConfig
get() = getConfigOrNull<RestarterConfig>()
?: throwInternal("Attempted to fetch a configuration of a disabled feature")

/**
* Builder of [RestarterConfig].
*
* @see [RestarterConfigBuilder.Companion.create]
*/
@ConfigDSL
@ExperimentalRestartApi
class RestarterConfigBuilder private constructor(
override val startArgs: List<String>,
) : RestarterConfigProps {

override var restartDelay: Duration = 1.seconds

/**
* Sets the time to wait before assuming all changes were compiled,
* so the application can be restarted with the new changes.
*
* Default: 1 second
*/
fun setRestartDelay(delay: JavaDuration): RestarterConfigBuilder {
this.restartDelay = delay.toKotlinDuration()
return this
}

/**
* Builds the [RestarterConfig], you can register the built configuration with [BConfigBuilder.registerModule].
*/
fun build(): RestarterConfig = object : RestarterConfig {
override val startArgs = this@RestarterConfigBuilder.startArgs
override val restartDelay = this@RestarterConfigBuilder.restartDelay
}

internal companion object {

@JvmSynthetic
internal fun create(args: Array<out String>): RestarterConfigBuilder {
return RestarterConfigBuilder(args.toList())
}
}
}

/**
* Registers the restarter module, enabling the feature.
*
* @param args The program arguments, they will be passed to the main method upon restarting
* @param block A block for further configuration
*
* @throws IllegalStateException If the module was already registered
*/
@ExperimentalRestartApi
fun BConfigBuilder.registerRestarter(args: Array<out String>, block: RestarterConfigBuilder.() -> Unit = { }) {
val config = RestarterConfigBuilder.create(args)
.apply(block)
.build()
registerModule(config)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.freya02.botcommands.restarter.internal

import io.github.freya022.botcommands.internal.core.restarter.RestartClassLoaderAdapter
import io.github.freya022.botcommands.internal.core.restarter.RestartClassLoaderAdapterFactory

internal class BCRestartClassLoaderAdapterFactory : RestartClassLoaderAdapterFactory {

override fun wrapOrNull(loader: ClassLoader): RestartClassLoaderAdapter? {
return if (loader is RestartClassLoader) {
BCRestartClassLoaderAdapter(loader)
} else {
null
}
}

private class BCRestartClassLoaderAdapter(private val loader: RestartClassLoader) : RestartClassLoaderAdapter {

override fun publicDefineClass(name: String, bytes: ByteArray): Class<*> =
loader.publicDefineClass(name, bytes)
}
}
Loading
Loading