Articles

Noms d'hôte docker

Utilisant Docker quotidiennement et travaillant sur plusieurs projets, je souhaitais harmoniser les configurations et utiliser des noms de domaines locaux pour accéder à mes conteneurs.

Il y a plusieurs solutions existantes pour faire ça, comme Traefik ou DNS Proxy Server.

Traefik fonctionne bien, apporte beaucoup de fonctionnalités, mais n'est pas très léger pour une utilisation sur un poste de travail.

DNS Proxy Server fonctionne pas trop mal mais pose problème sur la résolution des noms de domaines externes aux conteneurs. Un simple ping google.fr ne passe pas si on utilise pas le réseau en mode bridge.

Ne trouvant pas mon bonheur dans ces solutions, j'ai décidé de créer un petit script shell qui se chargerai de maintenir à jour le fichier /etc/hosts.

Le principe est simple : écouter les événements de Docker lorsque des conteneurs sont démarrés, arrêtés, détruits ou tués, et mettre à jour le fichier /etc/hosts avec la liste des conteneurs en fonctionnement, en se basant sur la configuration hostname des conteneurs et leur IP assignée.

Écouter les événements Docker

Créons un script bash :

echo '!#/bin/bash' > docker-hosts.sh && chmod +x docker-hosts.sh

Pour écouter les événements Docker, j'ai trouvé ce gist que j'ai adapté :

function listen_docker_events() {
    docker events --filter 'event=start' --filter 'event=stop' --filter 'event=kill' --filter 'event=destroy' | while read event
    do
        #update_hosts
    done
}

Ajoutons des marqueurs d'emplacement dans /etc/hosts afin de cloisonner la mise à jour des IPs. J'ai mis des marqueurs largement inspirés des marqueurs des recettes flex.

###> docker-hosts ###
###< docker-hosts ###

Dès qu'un conteneur est démarré ou arrêté, on mettra à jour la liste de correspondance des IPs et des noms d'hôtes dans notre fichier /etc/hosts.

Mise à jour de /etc/hosts

Pour la mise à jour du fichier /etc/hosts nous allons créer une fonction nommée update_hosts.

function update_hosts() {
    CONTAINERS=$(docker ps -q | awk '{ print $1 }')

    HOST_PLACEHOLDER_START='###> docker\-hosts ###'
    HOST_PLACEHOLDER_END='###< docker\-hosts ###'
    HOST_ITEMS=''

    while read -r CONTAINER; do
        HOST_ITEMS+=$(echo -e $(docker inspect  --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\t\t{{ .Config.Hostname }}' $CONTAINER))
        HOST_ITEMS+='\n'
    done <<< "$CONTAINERS"

    sed -i -e "/${HOST_PLACEHOLDER_START}/,/${HOST_PLACEHOLDER_END}/c\\${HOST_PLACEHOLDER_START}\n${HOST_ITEMS}${HOST_PLACEHOLDER_END}" /etc/hosts
}

Décomposons cette fonction :

CONTAINERS=$(docker ps -q | awk '{ print $1 }')

On récupère la liste des identifiants des conteneurs en cours de fonctionnement et on la stocke dans la variable CONTAINERS.

HOST_PLACEHOLDER_START='###> docker\-hosts ###'
HOST_PLACEHOLDER_END='###< docker\-hosts ###'
HOST_ITEMS=''

On définis les marqueurs qui serviront au remplacement et la variable qui contiendra la liste de correspondance des IPs et des noms d'hôte (HOST_ITEMS).

while read -r CONTAINER; do
# ...
done <<< "$CONTAINERS"

On boucle sur la liste des identifiants des conteneurs en cours de fonctionnement.

HOST_ITEMS+=$(echo -e $(docker inspect  --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\t\t{{ .Config.Hostname }}' $CONTAINER))

À l'aide de la commande docker inspect, on récupère les informations utiles de chaque conteneur : Adresse IP et nom d'hôte à l'aide du format suivant :

{{ if gt (len .NetworkSettings.Networks) 1 }}
    {{ range \$name,\$network := .NetworkSettings.Networks }}
        {{ \$hasUnderscore := gt (len (split \$name "_")) 0 }}
        {{ if \$hasUnderscore }}
            {{ range \$part := (split \$name "_") }}
                {{ if eq \$part "default" }}
                    {{ \$network.IPAddress }}
                {{ end }}
            {{ end }}
        {{ else }}
            {{ .NetworkSettings.Networks.IPAddress }}
        {{ end }}
    {{ end }}
{{ else }}
    {{ range .NetworkSettings.Networks }}
        {{ .IPAddress }}
    {{ end }}
{{ end }}
\t\t
{{ .Config.Hostname }}

On remplace tout le contenu situé entre le marqueur de début ###> docker-hosts ### et le marqueur de fin ###< docker-hosts ### par la liste des IPs et noms d'hôtes de la variable HOST_ITEMS.

sed -i -e "/${HOST_PLACEHOLDER_START}/,/${HOST_PLACEHOLDER_END}/c\\${HOST_PLACEHOLDER_START}${HOST_ITEMS}${HOST_PLACEHOLDER_END}" /etc/hosts
Vu que /etc/hosts n'est inscriptible qu'en ayant les droits root, on pourra tester le script en essayant sur un fichier host de test situé dans le même répertoire que le script : cat /etc/hosts > ./hosts

Ensuite, on remplace dans la commande sed le fichier sur lequel le remplacement se fera :

sed -i -e "[...]" ./hosts

Déclarer le daemon

Déclarons un service systemd pour démarrer automatiquement ce script lors du démarrage de l'OS.

# /etc/systemd/system/docker-host.service
[Unit]
Description=Docker host daemon
After=docker.service

[Service]
ExecStart=/bin/bash <CHEMIN_VERS_VOTRE_SCRIPT>/docker-hosts.sh
Restart=on-failure

[Install]
WantedBy=multi-user.target

On active ensuite le service et on le lance :

sudo systemctl enable docker-host.service
sudo systemctl start docker-host.service

Exemple avec docker-compose

On utilise un docker-compose.yml avec 2 services basiques :

version: '3'
services:
    hello-world-1:
        image: strm/helloworld-http
        container_name: hello-world-1
        hostname: hello-world-1.papsou

    hello-world-2:
        image: strm/helloworld-http
        container_name: hello-world-2
        hostname: hello-world-2.papsou

Notre fichier /etc/hosts ressemble à ça avant le lancement :

# /etc/hosts

127.0.0.1              localhost localhost.localdomain localhost4 localhost4.localdomain4
::1                    localhost localhost.localdomain localhost6 localhost6.localdomain6

# Some fixed IP

# [...]

###> docker-hosts ###
###< docker-hosts ###

On lance ces 2 services :

docker-compose up -d --build
Creating hello-world-1 ... done
Creating hello-world-2 ... done

Et voici le fichier après le lancement :

# /etc/hosts

127.0.0.1              localhost localhost.localdomain localhost4 localhost4.localdomain4
::1                    localhost localhost.localdomain localhost6 localhost6.localdomain6

# Some fixed IP

# [...]

###> docker-hosts ###
192.168.48.3		hello-world-2.papsou
192.168.48.2		hello-world-1.papsou
###< docker-hosts ###

On accède donc au premier service hello-world-1 en se rendant sur http://hello-world-1.papsou et sur hello-world-2 en se rendant sur http://hello-world-2.papsou.

On coupe les services :

docker-compose down && cat /etc/hosts
Stopping hello-world-2 ... done
Stopping hello-world-1 ... done
Removing hello-world-2 ... done
Removing hello-world-1 ... done
Removing network hello-world_default

Et voici le contenu de /etc/hosts après la coupure :

# /etc/hosts

127.0.0.1              localhost localhost.localdomain localhost4 localhost4.localdomain4
::1                    localhost localhost.localdomain localhost6 localhost6.localdomain6

# Some fixed IP

# [...]

###> docker-hosts ###
###< docker-hosts ###

Le script final

#!/bin/bash

function update_hosts() {
    CONTAINERS=$(docker ps -q | awk '{ print $1 }')

    HOST_PLACEHOLDER_START='###> docker\-hosts ###'
    HOST_PLACEHOLDER_END='###< docker\-hosts ###'
    HOST_ITEMS=''
    INSPECT_FORMAT_STRING=$(cat << EOT
{{ if gt (len .NetworkSettings.Networks) 1 }}
    {{ range \$name,\$network := .NetworkSettings.Networks }}
        {{ \$hasUnderscore := gt (len (split \$name "_")) 0 }}
        {{ if \$hasUnderscore }}
            {{ range \$part := (split \$name "_") }}
                {{ if eq \$part "default" }}
                    {{ \$network.IPAddress }}
                {{ end }}
            {{ end }}
        {{ else }}
            {{ .NetworkSettings.Networks.IPAddress }}
        {{ end }}
    {{ end }}
{{ else }}
    {{ range .NetworkSettings.Networks }}
        {{ .IPAddress }}
    {{ end }}
{{ end }}
\t\t
{{ .Config.Hostname }}
EOT
    )

    while read -r CONTAINER; do
        HOST_ITEMS+=$(echo -e $(docker inspect  --format "${INSPECT_FORMAT_STRING}" $CONTAINER))
        HOST_ITEMS+='\n'
    done <<< "$CONTAINERS"

    sed -i -e "/${HOST_PLACEHOLDER_START}/,/${HOST_PLACEHOLDER_END}/c\\${HOST_PLACEHOLDER_START}\n${HOST_ITEMS}${HOST_PLACEHOLDER_END}" /etc/hosts
}

function listen_docker_events() {
    docker events --filter 'event=start' --filter 'event=stop' --filter 'event=kill' --filter 'event=destroy' | while read event
    do
        update_hosts
    done
}

listen_docker_events

Portainer

Procédure d'installation officielle : https://portainer.readthedocs.io/en/latest/deployment.html

Installation / Exécution de portainer

Exécuter la commande suivante :

/usr/bin/docker run -d -p 9000:9000 \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v /opt/portainer:/data \
    --name portainer \
    portainer/portainer

La première fois, cela télécharge l'image de Portainer, ensuite, cela lancera uniquement le conteneur.

Création d'un service Systemd

Créer un service systemd permet de lancer automatiquement Portainer au démarrage de votre machine.

Créer le fichier /etc/systemd/system/portainer.service et ajouter le contenu ci-dessous :

[Unit]
Description=Start Portainer at startup
After=network.target
Requires=docker.service

[Service]
Type=simple
KillMode=none
ExecStart=/usr/bin/docker start -a portainer
ExecStop=/usr/bin/docker stop -t 5 portainer

[Install]
WantedBy=default.target

Puis charger et activer le service.

sudo systemctl daemon-reload
sudo systemctl start portainer.service
sudo systemctl enable portainer.service
J'ai fais un thème Stylish (chrome ou firefox) pour que l'interface adopte un style sombre : https://userstyles.org/styles/176993/portainer-dark-red.

Aperçu du thème stylish

Ouvrir directement les fichiers sources

Astuce très utile permettant d'ouvrir directement les fichiers concernés dans le profiler Symfony dans votre IDE préféré.

Configurer php

Configurer Xdebug pour que les liens de fichiers (utilisés par le debugger Symfony) soient ouvert via une url spécifique.

Dans le fichier de configuration /etc/php/7.0/conf.d/20-xdebug.ini, ajouter (ou modifiez si déjà présent)

xdebug.file_link_format = 'ide://%f:%l'

Cela générera des liens de ce type :

ide://YOUR_PROJECT_PATH/index.php:65

Ajouter l'URL handler

Votre environnement de bureau doit savoir comment gérer ce nouveau protocole.

Pour gnome, cherchez où se trouve le fichier mimeapps.list

find ~/ -name mimeapps.list
/home/PapsOu/.config/mimeapps.list

Éditez-le et ajoutez l'entrée suivante :

[Added Associations]
x-scheme-handler/ide=ide-handler.desktop;

La chaîne doit commencer par x-scheme-handler/ suivi du nom de protocole personnalisé.

Fichier .desktop

Créez le fichier desktop qui se chargera de lancer le un script correspondant au protocole personnalisé.

nano ~/.local/share/applications/ide-handler.desktop
[Desktop Entry]
Version=1.0
Type=Application
Exec=/PATH_TO_YOUR_IDE_SCRIPT/run-ide.sh %u
StartupNotify=false
Terminal=false
Categories=Utility;
MimeType=x-scheme-handler/ide
Name=IDE quick open
Comment=Launch IDE

Ne pas oublier un chmod +x sur ce fichier pour qu'il soit exécutable.

Script de lancement d'IDE

Créez le script qui sera exécuté lors des clics sur ces liens.

Mettez-le où bon vous semble, adaptez simplement la ligne Exec du fichier .desktop précédent.

run-ide.sh :

#!/bin/bash

# Strip de la partie « ide:// » de l'url
url="${1:6}"

# Atom
# /bin/atom ${fileName}:${lineNumber}

# Netbeans
# /bin/netbeans $url

# VSCode
/usr/bin/code -g ${fileName}:${lineNumber}:0

Appliquer les changements

Redémarrez votre navigateur et gnome (Alt + F2 puis R) et le clic sur un lien de fichier du debugger ouvrira votre IDE avec le fichier et placera le curseur à la ligne correspondante.

(Optionnel) Configurer Firefox

Si Firefox n'arrive pas à associer les liens ide:// à votre script, il est peut être nécessaire de modifier sa configuration.

Ajouter les entrées suivantes dans about:config pour que Firefox sache que ce protocole nouvellement créé existe.

network.protocol-handler.expose.ide: false
network.protocol-handler.external.ide: true
network.protocol-handler.warn-external.ide: false

Rediriger sur le referer

Une astuce simple. Lorsqu'on fait un traitement côté contrôleur et qu'on désire rediriger l'utilisateur vers la page appelante, il suffit de faire une redirection basée sur le referer.

On récupère d'abord le referer dans un controlleur :

$referer = $request->headers->get('referer');

Tout simple. Ensuite on fait la redirection comme d'habitude :

return $this->redirect($referer);

Et voila.

Services symfony

Créer un service dans Symfony est très facile. Je vais présenter 2 méthodes pour créer un service rapidement.

L'injection de dépendance

On le voit partout, on le dit très souvent : Il ne faut pas injecter le conteneur de services comme dépendance à un service (ou à tout autre élément). C'est un peu comme écraser une mouche à coup de batte de baseball.

Mais parfois, ça permet de se simplifier la vie. Par exemple dans un contrôleur, par habitude, on accède à n'importe quel service en faisant un petit :

$monService = $this->getContainer()->get('nom.du.service');

ou

$monService = $this->container->get('nom.du.service');

Grâce à ça, on accède directement aux services. Simple, efficace, pratique.

Service de fainéant

Ajoutons la définition de notre service dans la configuration de notre bundle.

services:
    papsou.acme.monService:
        class: PapsOu\AcmeBundle\DependencyInjection\Services\MonService
        calls:
            - [setContainer, ["@service_container"]]

Et notre classe de service :

namespace PapsOu\AcmeBundle\DependencyInjection\Services;

use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class MonService implements ContainerAwareInterface
{
    /**
     * @var ContainerInterface
     */
    private $container;

    /**
     * @inheritdoc
     */
    public function setContainer(ContainerInterface $container = null): void {
        $this->container = $container;
    }

    public function maFonction($parameters): void {
        // Votre fonction...
        $unService = $this->container->get('un.service');
    }
}

Pour résumer, on créé un service qui à besoin du conteneur de service de Symfony pour fonctionner (car on utilise le conteneur dans une méthode de la classe du service). Cette classe doit implémenter l'interface ContainerAwareInterface pour injecter le conteneur. On peut très bien aussi injecter le conteneur par le biais d'une injection par constructeur.

Voila, c'est rapide, et on change pas trop nos habitudes. Seulement, c'est bien crade !

Service « Propre »

Cette fois-ci, on va devoir réfléchir un minimum sur ce point : De quels services ai-je besoin ?

Pour notre exemple, on va dire qu'on a besoin de l'EntityManager de Doctrine et d'un logger. Allons-y !

La définition de notre service :

services:
    papsou.acme.monService:
        class: PapsOu\AcmeBundle\DependencyInjection\Services\MonService
        calls:
            - [setEm, ["@doctrine.orm.entity_manager"]]
            - [setLogger, ["@logger"]]

Et notre classe de service :

namespace PapsOu\AcmeBundle\DependencyInjection\Services;

use Doctrine\ORM\EntityManager;
use Symfony\Bridge\Monolog\Logger;

class MonService
{
    /**
     * @var EntityManager
     */
    private $_em;

    /**
     * @var Logger
     */
    private $logger;

    /**
     * @param EntityManager $em
     */
    public function setEm(EntityManager $em = null): void {
        $this->_em = $em;
    }

    /**
     * @param Logger $logger
     */
    public function setLogger(Logger $logger): void {
        $this->logger = $logger;
    }

    public function maFonction($parameters): void {
        // Votre fonction...
        $mesEntites = $this->_em
            ->getRepository('PapsOuAcmeBundle:Entite')
            ->findAll();
        // On fait des traitement sur nos entités
        // On va logger ces manipulations
        $this->logger->info('mon message');
    }
}

Voila. C'est un peu plus long à écrire, il faut savoir quels services on va devoir utiliser pour notre service. Mais c'est bien plus propre !

Si comme moi, vous avez fait pas mal de services en injectant directement le conteneur (par flemme probablement), je vous conseille de trouver quelques minutes pour optimiser tout ça et remplacer l'injection du conteneur en injectant uniquement les services que vous utilisez dans vos propres services. Juste histoire d'être plus carré, ou juste histoire de se dire « mes services sont propres ».

Captures d'écran

Petit script bash pour automatiser un peu les captures d'écran :

#!/bin/bash
NOW=`date +"%Y%m%d"`
TIME=`date +"%H%M%S"`
DIRECTORY="${HOME}/Images/Screenshots/${NOW}"
mkdir -p "${DIRECTORY}"
gnome-screenshot -a -f "${DIRECTORY}/${NOW}-${TIME}.png"

En associant le raccourci clavier de la touche Imp. écran à ce script, la capture par défaut prend une région et enregistre l'image dans le dossier: ~/Images/Screenshots/%DATE_FORMAT_US%/%DATE_FORMAT_US%-%TIME_FORMAT_US%.png

Sinon il y a Flameshot qui fonctionne très bien et permet de dessiner, écrire, flouter par dessus les régions capturées lors de la capture.

Rechercher une chaine de caractères

Astuce bien pratique, on recherche des occurrences d'une chaine de caractères dans des fichiers. comment les trouver facilement ?

Simplement avec la commande find couplée avec grep.

Deux façons :

find . | xargs grep 'ma chaine' -sl

Ou :

find ./ -exec grep -Hn "ma chaine" {} \;

Ou encore plus simple à retenir :

grep -Rin "ma chaine"

Pacman

Petite liste des commandes basiques de pacman

Commandes

Synchronisation (équivalent du apt-get update chez Debian) :

pacman -Sy

Mise à jour des paquets :

pacman -Su

Installer un paquet :

pacman -S nom_du_paquet

Recherche un paquet parmis ceux installés :

pacman -Qs nom_du_paquet

Recherche un paquet dans les dépôts :

pacman -Ss nom_du_paquet

Exemple

On va installer pkfile histoire de retrouver l'équivalent de yum whatprovides disponible sous fedora (et autres distribution utilisant YUM) :

pacman -Sy
:: Synchronisation des bases de données de paquets...
 core est à jour
 extra est à jour
 community est à jour
 alarm est à jour
 aur est à jour

pacman -Ss pkgf
extra/pkgfile 14-1
    a pacman .files metadata explorer

pacman -S pkgfile
résolution des dépendances...
recherche des conflits entre paquets...

Paquets (1): pkgfile-14-1

Taille totale de téléchargement : 0,02 MiB
Taille totale installé :           0,12 MiB

:: Procéder à l’installation ? [O/n] O
:: Récupération des paquets...
 pkgfile-14-1-armv6h       21,1 KiB   176K/s 00:00 [######################] 100%
(1/1) vérification des clés dans le trousseau      [######################] 100%
(1/1) vérification de l’intégrité des paquets      [######################] 100%
(1/1) chargement des fichiers des paquets          [######################] 100%
(1/1) analyse des conflits entre fichiers          [######################] 100%
(1/1) vérification de l’espace disque disponible   [######################] 100%
(1/1) installation de pkgfile                      [######################] 100%
==> Run ''pkgfile --update'' to initialize the database
synchronizing filesystem...

Voila, c'est installé. Maintenant il suffit juste de faire ce qu'on nous demande, c'est à dire :

pkgfile --update
:: Updating 5 repos...
  download complete: core                 [   442.8 KiB   452K/s  4 remaining]
  download complete: alarm                [    62.9 KiB  11.5K/s  3 remaining]
  download complete: aur                  [   103.0 KiB  18.4K/s  2 remaining]
  download complete: community            [     6.1 MiB   564K/s  1 remaining]
  download complete: extra                [     6.2 MiB   503K/s  0 remaining]
:: download complete in 12.66s            <    12.9 MiB  1044K/s  5 files    >
:: waiting for 2 processes to finish repacking repos...

Voila.

Maintenant, mettre à jour la base de pkgfile :

pkgfile -u

Et pour connaître le(s) paquet(s) qui fournis(sent) une commande particulière (exemple pour scp) :

pkgfile scp
core/openssh
extra/bash-completion

SSH Agent

Chose bien pratique, c'est de n'avoir qu'une seule fois à déverrouiller une clé lorsqu'on utilise ssh plusieurs fois par jours.

Par défaut, à chaque connexion ssh, il faut entrer le mot de passe associé à la clé. Quand on l'utilise 2 ou 3 fois par jours, ça peut aller. Mais si, comme dans mon cas, vous l'utilisez au minimum 20 fois par jours, ça devient très vite fatiguant.

L'astuce consiste simplement à ajouter la clé dans l'agent qui gère ces clés (ssh-agent ou seahorse-agent) :

ssh-add ~/.ssh/votre_cle_ssh
Enter passphrase for ~/.ssh/votre_cle_ssh:
Identity added: ~/.ssh/votre_cle_ssh (~/.ssh/votre_cle_ssh)

Et voila.

Seulement, cela fonctionnera uniquement pour la session en cours. Redémarrez, et vous devrez refaire cette manipulation.

Utilisant KDE, j'ai décidé d'utiliser le Kwallet qui gère les mots de passe de la session KDE. Il demande un seul mot de passe pour déverrouiller les autres qu'il gère, par exemple des calendriers Google calendar, etc.

Pour ce faire, ce n'est pas du tout compliqué.

Créez un script bash qui s’exécutera au démarrage de la session KDE :

nano ~/.kde/Autostart/ssh-add.sh
Sur certaines distributions, le dossier ~/.kde/ se nomme ~/.kde4/

Saisissez les commandes d'ajout d'identité pour votre clé (ou vos clés si vous en avez plusieurs) :

#!/bin/bash
ssh-add ~/.ssh/votre_cle_ssh_1 </dev/null
ssh-add ~/.ssh/votre_cle_ssh_2 </dev/null

Rendez le script exécutable :

chmod +x ~/.kde/Autostart/ssh-add.sh

Exécutez-le une fois pour la session courante (vous serez invité à saisir le mot de passe associé à votre clé) :

bash ~/.kde/Autostart/ssh-add.sh

Au prochain démarrage de votre session, le mot de passe de Kwallet vous sera demandé, et déverrouillera par la même occasion vos clé ssh.

Navigateur par défaut

Avoir plusieurs navigateurs, et ouvrir les liens dans le dernier qui a eu le focus.

La problématique

Pour travailler, j'utilise Chrome. Pour le perso, j'utilise Brave (mais bientôt de nouveau Firefox histoire de revenir dans le droit chemin).

Le week end, quand j'ouvre un lien depuis une application (exemple : Element), le navigateur du boulot s'ouvre, alors que j'avais mon navigateur perso ouvert.

Rien de très dérangeant en soit, mais cela évite de voir des notifications liées au travail lorsqu'on n'y pense pas (ou qu'on ne veut pas y penser).

Du coup, temporairement, je défini Brave comme navigateur par défaut, et je savoure la fin de mon week end.

Lundi matin, j'attaque le travail, et un lien ouvert à partir de mon client mail (Evolution) m'ouvre ce lien dans Brave, alors que Chrome est ouvert et actif...

Donc, recherche d'une solution pour corriger ce comportement.

La solution toute faite

Il faudra donc jouer avec les applications par défaut pour arriver à mes fins. Mes brèves recherches ne m'ont pas donnés de résultat pertinent pour résoudre cette petite problématique.

Donc rien de tout fait (à ma connaissance) pour répondre à cette problématique.

Alors hop, rédigeons nos scripts qui feront pile poil ce que l'on veut.

La logique

Lorsqu'on clique sur un lien, on va appeler un faux navigateur par défaut, qui se chargera d'appeler à son tour les vrais navigateurs, selon des critères plutôt simples : ouvrir le lien dans le dernier navigateur qui à été utilisé (dans la session courante).

Cas particulier, si aucun navigateur n'a été lancé en début de session, il faut un prompt pour demander quel navigateur lancer.

Comment ça va tourner ?

Lorsqu'on va cliquer sur un lien depuis une application qui n'est pas le navigateur, on va appeler notre faux navigateur par défaut (qu'on aura définis dans notre Desktop Environment, Gnome dans mon cas).

Ce faux navigateur déterminera quel à été le dernier navigateur en utilisation, ou demandera à l'utilisateur quel navigateur choisir si aucune n'a encore été utilisé. Puis il transférera le lien cible au vrai navigateur qui se lancera (si par encore démarré) ou ouvrira un nouvel onglet dans la dernière fenêtre active.

Les scripts

On va rédiger plusieurs scripts :

  • Le détecteur de navigateur actif : default-browser-focus-handler.sh
  • le faux navigateur : default-browser-launcher.sh
  • Les fichiers default-browser-focus-handler.desktop et browser-handler.desktop pour être géré par le Desktop Environment

Le détecteur de navigateur actif

A l'aide de l'outil xdotool, on va capturer le titre de la fenêtre en cours de focus, et se baser sur la fin du titre (qui pour les navigateurs, est le nom du navigateur) pour déterminer quel est le navigateur sélectionné.

On stockera cette information dans un fichier dans /tmp, qui sera utilisé par notre faux-navigateur pour savoir quel navigateur lancer.

Ce script devra tourner en arrière plan et sera lancé lors du démarrage du bureau.

### default-browser-focus-handler.sh

#!/bin/sh

echo '' > /tmp/default-browser

CURRENT_DEFAULT_BROWSER=$(cat /tmp/default-browser)

while [ true ]
do
    WINDOW=$(xdotool getwindowfocus getwindowname)

    echo $WINDOW

    if [[ "$WINDOW" == *"- Google Chrome" && $CURRENT_DEFAULT_BROWSER != "google-chrome" ]]
    then
        echo "google-chrome" > /tmp/default-browser
        CURRENT_DEFAULT_BROWSER="google-chrome"
    elif [[ "$WINDOW" == *"– Brave" && $CURRENT_DEFAULT_BROWSER != "brave-browser" ]]
    then
        echo "brave-browser" > /tmp/default-browser
        CURRENT_DEFAULT_BROWSER="brave-browser"
    elif [[ "$WINDOW" == *"— Firefox" && $CURRENT_DEFAULT_BROWSER != "firefox" ]]
    then
        echo "firefox" > /tmp/default-browser
        CURRENT_DEFAULT_BROWSER="firefox"
    fi

    sleep 2
done

Et voici le fichier .desktop qui permet de lancer le script précédent au démarrage du bureau : (le placer dans ~/.local/share/applications/)

### default-browser-focus-handler.desktop

#!/bin/sh
[Desktop Entry]
Version=1.0
Terminal=false
Type=Application
Name=Browser focus handler
Exec=<chemin_vers_scripts>/default-browser-focus-handler.sh

Le faux navigateur

Ce script va transférer le lien cible au vrai navigateur selon les cas.

On utilise Xdialog pour construire une fenêtre comportant les navigateurs gérés et forcer le choix lorsqu'aucun navigateur n'a été encore démarré.

Avec which on détermine à quel exécutable correspond la chaîne de texte représentant le nom du navigateur cible.

### default-browser-launcher.sh

#!/bin/sh

CURRENT_DEFAULT_BROWSER=$(cat /tmp/default-browser)

TARGET_BROWSER=''
if [[ $CURRENT_DEFAULT_BROWSER == "" ]]
then
    TARGET_BROWSER=$(Xdialog \
        --stdout \
        --radiolist \
        "Choose browser" 200x200 200 \
        brave-browser Brave 1 \
        google-chrome Chrome 2 \
        firefox Firefox 3
    )
    echo "$TARGET_BROWSER" > /tmp/default-browser
else
    TARGET_BROWSER=$CURRENT_DEFAULT_BROWSER
fi

$(which $TARGET_BROWSER) $@

Pour le définir par défaut, il faut le référencer en tant que navigateur. Cela se fait entièrement dans le .desktop : (le placer dans ~/.local/share/applications/)

### browser-handler.desktop

#!/usr/bin/env xdg-open
[Desktop Entry]
Version=1.0
Terminal=false
Type=Application
Name=Browser handler
Exec=<chemin_vers_scripts>/default-browser-launcher.sh %u
Icon=

Ensuite, on le défini par défaut à l'aide de la commande suivante :

xdg-settings set default-web-browser browser-handler.desktop

Cela va rajouter automatiquement le MimeType dans notre .desktop :

### browser-handler.desktop

#!/usr/bin/env xdg-open
[Desktop Entry]
Version=1.0
Terminal=false
Type=Application
Name=Browser handler
Exec=<chemin_vers_scripts>/default-browser-launcher.sh %u
Icon=
MimeType=x-scheme-handler/unknown;x-scheme-handler/about;x-scheme-handler/https;x-scheme-handler/http;text/html;

Conclusion

Cet ensemble de script répond tout à fait à mon besoin, et je n'ai pas encore rencontré de comportement étrange ou non-désirés.

MdBook

MdBook est un GitBook like écrit en Rust, mais, contrairement à GitBook, il a gardé sa simplicité.

Le principe est simple, vous écrivez vos articles / chapitres en MarkDown, vous organisez votre sommaire, et vous générer le book en format html.

La documentation officielle est faite avec mdbook ainsi que ce « blog » que vous êtes en train de lire.

Installation

J'ai opté pour la méthode la plus simple : récupérer le binaire linux sur leur dépôt git : https://github.com/rust-lang/mdBook/releases et extraire le binaire dans un dossier qui est dans mon PATH.

Et voila, mdbook est installé.

CLI

C'est donc un outil en CLI très simple à utiliser.

Comme tout bon outil CLI, une aide très claire est présente :

mdbook -h
mdbook v0.4.7
Mathieu David <mathieudavid@mathieudavid.org>
Creates a book from markdown files

USAGE:
    mdbook [SUBCOMMAND]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

SUBCOMMANDS:
    build    Builds a book from its markdown files
    clean    Deletes a built book
    help     Prints this message or the help of the given subcommand(s)
    init     Creates the boilerplate structure and files for a new book
    serve    Serves a book at http://localhost:3000, and rebuilds it on changes
    test     Tests that a book's Rust code samples compile
    watch    Watches a book's files and rebuilds it on changes

For more information about a specific command, try `mdbook <command> --help`
The source code for mdBook is available at: https://github.com/rust-lang/mdBook

Initialisation

Pour commencer, on execute la commande suivante :

mdbook init

Do you want a .gitignore to be created? (y/n)
y
What title would you like to give the book? 
Example book
2021-04-24 16:41:57 [INFO] (mdbook::book::init): Creating a new book with stub content

All done, no errors...

On se retrouve avec la structure de fichier suivante :

.
├── book
├── book.toml
└── src
    ├── chapter_1.md
    └── SUMMARY.md
Le dossier book/ est un dossier généré. Tout son contenu sera intégralement remplacé lors des constructions du book.

Construction du book

Pour générer le book en html, on éxécute simplement la commande suivante :

mdbook build

2021-04-24 17:07:57 [INFO] (mdbook::book): Book building has started
2021-04-24 17:07:57 [INFO] (mdbook::book): Running the html backend

Le book est généré dans le dossier <projet>/book/ :

./book
├── 404.html
├── ayu-highlight.css
├── book.js
├── chapter_1.html
├── clipboard.min.js
├── css
│   └── [...]
├── elasticlunr.min.js
├── favicon.png
├── favicon.svg
├── FontAwesome
│   └── [...]
├── fonts
│   └── [...]
├── highlight.css
├── highlight.js
├── index.html
├── mark.min.js
├── print.html
├── searcher.js
├── searchindex.js
├── searchindex.json
└── tomorrow-night.css

Il vous suffit d'ouvrir le fichier index.html pour voir le book.

Construction à la volée

Lors de la rédaction de vos articles, vous ne voudrez pas faire manuellement un mdbook build à chaque changement.

Pour ce faire, la commande mdbook watch permet de reconstruire le book à chaque sauvegarde d'article.

Cela permet d'avoir le rendu (moyennant un rafraîchissement manuel) dans son navigateur pendant qu'on rédige un article dans son IDE préféré.

Éléments markdown

Images

Image

    
    {{ image-centered "../img/avatar-new.png"}}
    

Image centrée et réduite

    
    {{ image-centered "../img/avatar-new.png" 25% }}
    

Image déformée

    
    {{ image-centered "../img/avatar-new.png" 200px 50px }}
    

Image réduite

    
    {{ image-centered "../img/avatar-new.png" 25% }}
    

Blocs

Note

{{ block note }}
    une note
{{ endblock }}
une note

Warning

{{ block warning }}
    un warning
{{ endblock }}
un warning

Danger

{{ block danger }}
    un danger
{{ endblock }}
un danger

Success

{{ block success }}
    une note
{{ endblock }}
une note

Citation

Une citation

Une citation

Une citation

Une citation

Code

En ligne

un bout de `code` en ligne

un bout de code en ligne

En bloc

Bash

```bash
echo "Test"
```
echo "Test"

Php

```php
namespace My\Namespace;

use DateTime;

class MyClass
{
    public function myFunction(): void
    {
        // do nothing
    }
}
```
namespace My\Namespace;

use DateTime;

class MyClass
{
    public function myFunction(): void
    {
        // do nothing
    }
}