Debug annotation processor in Kotlin

Debug annotation processor in Kotlin cover

I’ve recently revisited a library that I’ve done a while back using annotation processor and as soon as I opened it and started reading the code I’ve remembered how difficult it was to debug it at the time. So, a few years later let’s see what changed and how can we use the debugger instead of just adding some log messages.

Log messages

In order to print messages during compile time of your annotation processor you need to use ProcessingEnvironment.getMessager().printMessage(…) . The ProcessingEnvironment object is defined in the AbstractProcessor class and you can easily access it when you extend it.

class GenerateProcessor : AbstractProcessor() {
    
  override fun process(type: MutableSet<out TypeElement>?, 
                       roundEnv: RoundEnvironment?): Boolean {

  processingEnv.*messager*.printMessage(WARNING, "Processing")
  ...

Although this method can receive a couple of arguments, typically we send it just two:

void printMessage(Diagnostic.Kind kind, CharSequence charSequence);

Kind corresponds to the type of log message (sorted by severity, being the first one the most critical):

ERROR
When something is invalid or it’s missing. If this type is defined the compilation process is aborted and the error displayed on the console log.

WARNING
When something it’s not 100% correct but it’s not enough to stop the compilation process.

MANDATORY_WARNING
Similar to a warning, but is mandated by the tool’s specification.

NOTE
Informative message — for instance, when a task starts/ ends.

OTHER
When the message does not fit in any of the above categories.

Log messages
Log messages

Debug

Similar to attaching the debugger to an application that’s running on your smartphone or emulator you can do the same thing during the compilation process — which is truly incredible and really helpful, trust me.

How can you do this?

There are a couple of steps involved in this task — I’m currently using Kotlin v1.3.41 and Gradle 5.1.1 (with Android Plugin 3.4.2):

  1. We’re going to first create a Remote Configuration so you can attach it later to debug your annotation processor code:
    1. Go to Edit Configurations… (Run/ Debug Configurations)
    2. Click on the plus sign (“+”)
    3. Select Remote
    4. You should have a new configuration added that’s similar to this one:
    Remote debugger configuration
    Remote debugger configuration

    I’ve used the default configuration here — I’ve just changed the default name to Debugger so it will be easier to spot on Android Studio actions bar.

  2. On your gradle.properties file you’ll need to add:
    kapt.use.worker.api=true
    

    otherwise, it won’t stop on your breakpoints.

  3. Now that everything is prepared you can start compiling the app by entering the following command in Terminal (I typically use the one in Android Studio):
    ./gradlew --no-daemon -Dorg.gradle.debug=true -Dkotlin.daemon.jvm.options="-Xdebug,-Xrunjdwp:transport=dt_socket\,address=5005\,server=y\,suspend=n" :clean assemble
    

    But what does this all mean?

Gradle daemon

-Dorg.gradle.debug=true

Starts a new Gradle process that will run the build with remote debugging enabled, listening on port 5005 by default. It’s equivalent to call:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005

If you look at the suspend attribute you can see that it’s true, meaning that the virtual machine will be suspended until a debugger is attached.

Kotlin daemon

-Dkotlin.daemon.jvm.options="-Xdebug,-Xrunjdwp:transport=dt_socket\,address=5005\,server=y\,suspend=n"

Kotlin starts a daemon process that listens on port 5005 for a debugger

transport
Name of the transport used to connect to the debugger application (required). The default value if not set is none.

address
The transport address for the connection. If server option is set to n the debugger will attempt to be attached at this address; if instead is enabled - y it will listen for a connection at this port (if server=n this option is required). The default value if not set is “” (empty).

server
If y it will listen for a debugger to be attached; otherwise, will attach to the debugger application at the specified address (not required). The default value if not set is n.

suspend
It defines the policy used on VMStartEvent if y it will be SUSPEND_ALL otherwise will be SUSPEND_NONE (not required). The default value if not set is y.

And the last instructions are responsible to clean and compile the current project.

:clean assemble

After entering the command this process will wait until you attach the debugger — this guarantees that you don’t miss the breakpoint by not being fast enough:

Compiling the project
Compiling the project

Before, attaching the debugger don’t forget to check if you’ve got the right configuration selection. Once confirmed, just hit debug.

Remote config “Debugger” created
Remote config “Debugger” created

Once the daemon detects that the debugger is attached the compilation process continues until it detects your breakpoint.

Debugging an annotation processor library
Debugging an annotation processor library

Troubleshooting

I’ve spent some time trying to understand why this wasn’t working correctly in the first place — some useful commands that I’ve been using during this process were:

  • jps — Java Virtual Machine Process Status Tool
$ jps

To list all JVM’s that are currently running. Before understanding the right commands to use I kept having Connection Refused errors when I tried to attach the debugger — this happened because I’d left a daemon active that was already bound to the port 5005.

$ jps
  5541 GradleDaemon
  294 
  5671 Jps

You can easily stop it by either call ./gradlew —-stop or kill(pid) in this case, it would be kill(5541) .

For more information:

Do you have a better approach? Something didn’t quite work with you? Feel free to send me a message 🙂.