Ce billet présente une méthode pour utiliser le débogueur Intellij (exécution avec points d’arrêts et pas à pas) pour une application TamboUI.

Qu’est-ce que TamboUI ? Link to heading

TamboUI est une librairie java permettant de construire des interfaces utilisateurs dans les terminaux. A l’instar de Ratatui pour Rust ou bien Textual pour Python, la librairie permet de construire des interfaces texte riches pour les terminaux avec toutes les fonctionnalités modernes pour développer une CLI en java.

Voici un exemple de TUI qu’on peut créer à l’aide de TamboUI : Demo widget arborescence de fichiers avec TamboUI

Il y a de nombreux autres exemples ici

Une application java TamboUI peut se révéler très simple à écrire grâce aux nombreux composants disponibles qui permettent de disposer de toutes sortes d’interfaces utilisateurs déjà constituées : c’est notamment le cas si on utilise l’API Toolkit DSL On peut afficher un tableau très facilement avec quelque lignes de code :

//DEPS dev.tamboui:tamboui-toolkit:0.4.0
//DEPS dev.tamboui:tamboui-panama-backend:0.4.0

package poc;

import dev.tamboui.toolkit.app.ToolkitApp;
import dev.tamboui.toolkit.element.Element;
import dev.tamboui.widgets.table.Row;
import dev.tamboui.widgets.table.TableState;

import static dev.tamboui.toolkit.Toolkit.fill;
import static dev.tamboui.toolkit.Toolkit.table;

public class HelloTamboUI extends ToolkitApp {

    @Override
    protected Element render() {
        TableState state = new TableState();
        state.select(3);
        return table()
                .onBlue()
                .white()
                .header("Id", "Title", "Author")
                .columnSpacing(2)
                .state(state)
                .widths(fill(), fill(), fill())
                .rows(Row.from("1", "The Pillars of the Earth", "Ken Follett"),
                        Row.from("2", "World Without End", "Ken Follett"),
                        Row.from("3", "Les Trois Mousquetaires", "Alexandre Dumas"),
                        Row.from("4", "Le Comte de Monte-Cristo", "Alexandre Dumas"),
                        Row.from("5", "Quatre-vingt-treize", "Victor Hugo"),
                        Row.from("6", "Les Misérables", "Victor Hugo")
                )
                .title("Welcome to TamboUI!")
                .rounded();
    }

    public static void main(String[] args) throws Exception {
        new HelloTamboUI().run();
    }
}

Comment déboguer une application TamboUI ? Link to heading

Certaines applications peuvent être beaucoup plus complexes et nécessiter un débogage. Il n’est cependant pas possible d’exécuter en mode débogage une application TamboUI dans un IDE tel que IntelliJ car il n’y a pas de terminal sous-jacent pour recevoir les IHM de tamboUI.

Voyons comment exécuter une application TamboUI dans un terminal en dehors de l’IDE et guider l’exécution pas à pas depuis IntelliJ.

NB : l’exemple développé repose sur le code de la version 0.4.0 de TamboUI : il est possible que l’utilisation du débogage ne soit plus forcément nécessaire pour comprendre ce qui passe dans une version ultérieure

Exécution avec jbang Link to heading

La démonstration se fera en éxécutant l’application TamboUI à l’aide de jbang : c’est un outil bien pratique pour exécuter une application java en ligne de commande sans avoir à reconstituer le classpath (ce que l’IDE fait habituellement). jbang va prendre en charge :

  • la compilation de la classe java
  • la récupération des dépendances pour compléter le classpath de l’application à l’exécution
  • la complexité des options de la commande java en proposant des options plus simples à utiliser pour le développeur

Les dépendances sont précisées en-tête de la classe java à travers des commentaires : comme ceci :

//DEPS dev.tamboui:tamboui-toolkit:0.4.0
//DEPS dev.tamboui:tamboui-panama-backend:0.4.0

Le minimum requis pour les dépendances d’une application TamboUI est de posséder un niveau d’API (ici tamboui_toolkit) et un backend permettant l’affichage effectif dans le terminal (ici tamboui-panama-backend).

Une simple commande permet de lancer l’application (après une compilation qui va être effectuée automatiquement) :

jbang /path/toTamboUIApplication.java

Si tout se passe bien, l’application se lance dans le terminal. Voyons ici un exemple dans lequel l’application ne démarre pas :

$ jbang src/main/java/poc/HelloTamboUI.java 
[jbang] Building jar for HelloTamboUI.java...
Exception in thread "main" dev.tamboui.terminal.BackendException: No BackendProvider found on classpath.
Add a backend dependency such as tamboui-jline3-backend or tamboui-panama-backend.
        at dev.tamboui.terminal.BackendFactory.tryProviders(BackendFactory.java:139)
        at dev.tamboui.terminal.BackendFactory.create(BackendFactory.java:91)
        at dev.tamboui.tui.TuiRunner.create(TuiRunner.java:186)
        at dev.tamboui.toolkit.app.ToolkitRunner.create(ToolkitRunner.java:122)
        at dev.tamboui.toolkit.app.ToolkitApp.run(ToolkitApp.java:100)
        at poc.HelloTamboUI.main(HelloTamboUI.java:44)

Pourtant l’en-tête de la classe HelloTamboUI.java comporte bien la ligne //DEPS dev.tamboui:tamboui-panama-backend:0.4.0 : au moins un backend est présent dans le classpath : exécutons l’application en mode débogage pour comprendre ce qui se passe dans la classe BackendFactory.

Préparer l’exécution Remote JMV Debug dans IntelliJ Link to heading

Le message d’erreur ci-dessus produit dans la méthode BackendFactory.tryProviders suggère que la collection providers dans la méthode BackendFactory.create est vide : en déboguant l’appel à SafeServiceLoader.load, on pourra certainement en comprendre la raison (on ne force pas le backend avec la propriété tamboui.backend ni avec une variable TAMBOUI_BACKEND dans l’exécution en cours)

On met donc un point d’arrêt sur la méthode SafeServiceLoader#load(java.lang.Class<S>) dans IntelliJ :

Point d’arrêt sur la méthode SafeServiceLoader#load

Configuration de l’option --debug et lancement Link to heading

La JVM qui exécute l’application TamboUI est lancée par jbang, on va donc passer une option à jbang pour réclamer une exécution en mode débogage de l’application : c’est l’option --debug :

$ jbang --debug  src/main/java/poc/HelloTamboUI.java
Listening for transport dt_socket at address: 4004

L’utilisation de l’option --debug de jbang revient ici à passer l’option -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=4004 à la JVM. L’utilisation de cette option permet de contrôler l’exécution de l’application via des points d’arrêt et du pas à pas par le débogueur selon le protocole défini pour la Java Platform Debugger Architecture (JPDA). À noter que l’option suspend=y suspend l’exécution courante de l’application jusqu’à ce qu’un débogueur permettant le contrôle de l’exécution se connecte (sinon l’application pourrait se terminer avant que les points d’arrêt aient pu être transmis). La connection se fait ici sur le port réseau 4004 (port déterminé par jbang).

Débogage Link to heading

Une fois l’application lancée et en attente d’un débogueur, il faut y connecter le débogueur de IntelliJ, on peut procéder de la sorte

  • Menu > Run > Attach to process… (CTRL+ALT+5)
  • Dans la fenêtre qui s’affiche, sélectionner le bon processus java (celui qui mentionne le nom de la classe principale, ici poc.HelloTamboUI)
  • Cliquer sur Attach with Java Debugger
  • L’application TamboUI est débloquée et la vue de débogage s’ouvre sur le point d’arrêt posé sur la méthode SafeServiceLoader.load

SafeServiceLoader.load dans le débogueur Intellij

Origine de l’erreur Link to heading

  • On se rend dans la méthode dev.tamboui.util.SafeServiceLoader#load(java.lang.Class<S>, java.util.function.Consumer<java.lang.Throwable>) avec Step into (F7)
  • On suit pas à pas l’exécution de cette méthode (Step over (F8) ) jusqu’à se retrouver dans un bloc catch :

Erreur catchée dans la méthode SafeServiceLoader.load

Si on lit le message de l’erreur, il est inscrit :

"java.lang.UnsupportedClassVersionError: dev/tamboui/backend/panama/PanamaBackendProvider has been compiled by a more recent version
of the Java Runtime (class file version 66.0), this version of the Java Runtime only recognizes class file versions up to 65.0"`

L’explication est que la version de java avec laquelle on exécute l’application (java 21) est trop ancienne pour utiliser le backend panama de TamboUI (qui nécessite java 22+). Celui-ci repose en effet sur la JEP 454 qui a été livrée avec Java 22.

On voit également que la méthode dev.tamboui.terminal.BackendFactory#create a appelé le SafeServiceLoader sans handler pour gérer les erreurs lors de l’instanciation des providers de backend, ce qui fait que l’erreur est ignorée et que l’application échoue sans explication précise. Une gestion des erreurs de type UnsupportedClassVersionError pourrait être proposée afin d’avertir l’utilisateur que la version de java n’est pas la bonne : la méthode dev.tamboui.util.SafeServiceLoader#load(java.lang.Class<S>, java.util.function.Consumer<java.lang.Throwable>) permet cela.

Résolution Link to heading

Lançons l’application avec java 25 :

  • soit en mettant java 25 dans le PATH
  • soit en définissant une variable d’environnement JAVA_HOME
  • soit avec des options de jbang pour préciser la version de java à utiliser (peut déclencher le téléchargement d’un JDK)

En modifiant la version de java dans le PATH avec un outil comme mise, la commande jbang reste la même. Voici le résultat :

Exemple de terminal UI avec TamboUI

Conclusion Link to heading

  • Si l’IDE utilisé n’offre pas un terminal complet comme c’est le cas avec IntelliJ, il n’est pas possible d’exécuter une application TamboUI directement dans l’IDE
  • Une solution pratique est d’exécuter l’application dans un terminal à part.
  • Des outils comme maven avec mvn compile exec:java ou bien jbang permettent d’exécuter l’application facilement en assurant la compilation au besoin et en ajoutant automatiquement les dépendances nécessaires au classpath
  • On peut déboguer l’application exécutée dans le terminal (dans une JVM non contrôlée par l’IDE) en permettant à l’IDE de piloter l’exécution suivant les points d’arrêt et les instructions de pas à pas dans l’IDE en lançant la JVM de l’application TamboUI avec la bonne option.
  • jbang propose une option simple pour déboguer des applications : --debug