Skip to content

Category archive for: Linux

Se préparer à l’arrivée de Java 9 avec docker et maven

Comme vous le savez peut-être, Java 9 se rapproche (feature complete annoncé pour le 10 décembre 2015 et general availability le 22 septembre 2016, plus d’infos sur la page du projet jdk9).

docker-logo

Il est donc temps de commencer à s’y intéresser, mais peut-être préférez-vous ne pas « polluer » votre environnement avec cette installation, alors pourquoi ne pas utiliser docker ?

Dans ce post, nous allons préparer un conteneur docker fournissant :

  • la version en cours du jdk 9
  • et maven 3.3.3

Création d’un conteneur docker pour le JDK 9

Autant ne pas réinventer la roue, je suis parti d’un Dockerfile défini dans l’article suivant :Build OpenJDK 9 using Docker

J’ai un peu refactoré l’ensemble pour regrouper les installations de package et défini un utilisateur dédié au développement pour ne pas travailler en root.

Voici le résultat :

FROM phusion/baseimage:latest

# Pre-install
RUN \
apt-get update && \
apt-get install -y \
libxt-dev zip pkg-config libX11-dev libxext-dev \
libxrender-dev libxtst-dev libasound2-dev libcups2-dev libfreetype6-dev \
mercurial ca-certificates-java build-essential wget && \
rm -rf /var/lib/apt/lists/*

# User
RUN export uid=1000 gid=1000 && \
mkdir -p /home/javadev && \
echo « javadev:x:${uid}:${gid}:JavaDev,,,:/home/javadev:/bin/bash » >> /etc/passwd && \
echo « javadev:x:${uid}: » >> /etc/group && \
echo « javadev ALL=(ALL) NOPASSWD: ALL » > /etc/sudoers.d/javadev && \
chmod 0440 /etc/sudoers.d/javadev && \
chown ${uid}:${gid} -R /home/javadev

ENV JAVA_HOME=/opt/java-bin
ENV PATH=$JAVA_HOME/bin:$PATH

# We need JDK8 to build
RUN \
wget –no-check-certificate –header « Cookie: oraclelicense=accept-securebackup-cookie » http://download.oracle.com/otn-pub/java/jdk/8u65-b17/jdk-8u65-linux-x64.tar.gz

RUN \
tar zxvf jdk-8u65-linux-x64.tar.gz -C /opt

# Let’s get JDK9
RUN \
cd /tmp && \
hg clone http://hg.openjdk.java.net/jdk9/jdk9 openjdk9 && \
cd openjdk9 && \
sh ./get_source.sh

RUN \
cd /tmp/openjdk9 && \
bash ./configure –with-cacerts-file=/etc/ssl/certs/java/cacerts –with-boot-jdk=/opt/jdk1.8.0_65

RUN \
cd /tmp/openjdk9 && \
make clean images

RUN \
cd /tmp/openjdk9 && \
cp -a build/linux-x86_64-normal-server-release/images/jdk \
/opt/openjdk9

RUN \
cd /tmp/openjdk9 && \
find /opt/openjdk9 -type f -exec chmod a+r {} + && \
find /opt/openjdk9 -type d -exec chmod a+rx {} +

ENV PATH /opt/openjdk9/bin:$PATH
ENV JAVA_HOME /opt/openjdk9

# Maven
RUN mkdir /apache-maven
RUN curl -fSL http://apache.mirrors.ovh.net/ftp.apache.org/dist/maven/maven-3/3.3.3/binaries/apache-maven-3.3.3-bin.tar.gz -o maven.tar.gz \
&& tar -xvf maven.tar.gz -C apache-maven –strip-components=1 \
&& rm maven.tar.gz*

ENV PATH /opt/openjdk9/bin:/opt/apache-maven/bin:$PATH

USER javadev
WORKDIR /home/javadev
VOLUME /home/javadev

(aussi disponible sur github : docker-jdk9-maven)

La création du conteneur est classique :

> docker build -t gcastel/openjdk9 .

à lancer dans le répertoire du Dockerfile.

Utilisation du conteneur

Tout s’est bien déroulé, pas d’erreurs ?
Il s’agit à l’heure actuelle d’un work in progress, on peut donc toujours avoir des surprises.

Si tout va bien, nous pouvons tester ce conteneur :

> docker run -it gcastel/openjdk9 java -version
openjdk version « 1.9.0-internal »
OpenJDK Runtime Environment (build 1.9.0-internal-_2015_10_20_08_31-b00)
OpenJDK 64-Bit Server VM (build 1.9.0-internal-_2015_10_20_08_31-b00, mixed mode)

Voilà, vous pouvez désormais compiler vos projets avec ce container, par exemple en montant un répertoire de travail local (localdir) comme répertoire home :

> docker run -v localdir:/home/javadev -it gcastel/openjdk9 bash

Cool ! Maintenant, occupons-nous de maven avec un petit projet utilisant une fonctionnalité de java 9 (la nouvelle API Process).

J’ai créé un petit projet de test disponible sur github.

La classe principale sera toute simple :

package fr.gcastel;

public class TestProcessAPI {
  public static void main(String[] args) throws Exception {
        System.out.println("Your pid is " + ProcessHandle.current().getPid());
  }
}

Pas besoin de dépendances particulières, on se contentera de prévenir maven qu’on est en java 9, cf. pom.xml:

  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.3.2</version>
    <configuration>
      <source>1.9</source>
      <target>1.9</target>
    </configuration>
  </plugin>

Allons-y, nous pouvons tester :

> mvn compile

Et si tout est ok :

> cd target/classes
> java fr.gcastel.TestProcessAPI
Your pid is 24

Une dernière chose : si vous souhaitez aider la communauté java, mettez en place un jenkins dédié à la compilation de vos projets avec le JDK 9 dès qu’il sera disponible en release candidate. N’hésitez pas, ça peut permettre d’éviter des problèmes tels que ceux rencontrés par lucene à la sortie de Java 7.

Références :

UPnP, Linux et Freebox : un peu de « hacking » pour trouver les URIs de ressources

Alors, voici mon problème initial :

J’utilise un media center pour accéder au flux TV d’une Freebox Revolution. Cet accès se fait via UPnP. Or, à chaque redémarrage de la Freebox, l’URI de la ressource UPnP change et je dois donc reconfigurer ma source TV.

Le media center utilise Kodi (ex-XBMC) sur un Raspberry Pi (cf. distribution OpenElec). Mon idée est donc de scanner les ressources UPnP au démarrage pour modifier automatiquement le chemin de ma source TV.

Cette URI a le format suivant (en l’occurrence dans le fichier ~/.kodi/userdata/guisettings.xml) :

upnp://abcdefgh-8b30-359b-01e9-d706ce4bf569/0/0/106/

et je cherche à trouver la partie suivante : « 0/0/106/ » qui correspond au chemin de la ressource « Freebox TV ».

Recherche des serveurs

Première étape : trouver les serveurs UPnP du réseau pour accéder à la Freebox.

Pour trouver les serveurs UPnP disponibles sur le réseau, vous pouvez utiliser gssdp-discover (issu de gupnp-tools) pour lister les ressources :

> gssdp-discover --timeout=3

Cette commande retournera une liste de ressources (toutes celles qui auront répondu en moins de 3 secondes), dont celle qui nous intéresse :

resource available
  USN:      uuid:abcdefgh-8b30-359b-01e9-d706ce4bf569::urn:schemas-upnp-org:service:ContentDirectory:1
  Location: http://192.168.0.254:52424/device.xml

Recherche des services

Une fois le serveur trouvé, il nous faut le descriptif du service ContentDirectory.
On utilise donc l’URL trouvée précédemment :

> wget http://192.168.0.254:52424/device.xml
> cat device.xml
...
<service>
<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>
<serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>
<controlURL>/service/ContentDirectory/control
</controlURL>
<eventSubURL>/service/ContentDirectory/event
</eventSubURL>
<SCPDURL>/service/ContentDirectory/scpd</SCPDURL>
</service>
...

Nous avons donc un ensemble d’URIs, dont une qui nous donnera le descriptif des APIs du service (le SCPD) :

> wget http://192.168.0.254:52424/service/ContentDirectory/scpd
> cat scpd
...

le XML obtenu contient les différentes APIs utilisables, dont celle qui nous intéresse Browse :

<action>
<name>Browse</name>
<argumentList>
  <argument>
    <name>ObjectID</name>
    <direction>in</direction>
    <relatedStateVariable>
       A_ARG_TYPE_ObjectID
    </relatedStateVariable>
  </argument><argument>
    <name>BrowseFlag</name>
    <direction>in</direction>
    <relatedStateVariable>
       A_ARG_TYPE_BrowseFlag
    </relatedStateVariable>
  </argument>
  ...
  <argument>
    <name>TotalMatches</name>
    <direction>out</direction>
    <relatedStateVariable>
       A_ARG_TYPE_Count
    </relatedStateVariable>
  </argument><argument>
    <name>UpdateID</name>
    <direction>out</direction>
    <relatedStateVariable>
       A_ARG_TYPE_UpdateID
    </relatedStateVariable>
  </argument>
</argumentList>
</action>

Nous avons ainsi l’ensemble des informations nécessaires pour adresser le service :

  • L’URL
  • Le chemin du service que nous recherchons
  • Les paramètres à lui fournir
  • Les informations qu’il nous retournera

Requêter le service « Browse »

Voilà, maintenant on peut rassembler tous les morceaux !

Nous allons devoir requêter le service en lui fournissant les bons paramètres. On a déjà de bonnes informations grâce au descriptif XML. On peut en rassembler encore plus en effectuant un sniffing réseau durant une recherche UPnP. En l’occurrence, un petit tcpdump durant un browsing UPnP est très informatif :

> tcpdump -s 0 -A -vvv host 192.168.0.254 and tcp port 52424

Je ne recopierai pas le détail des informations obtenues, mais juste la requête SOAP que j’ai pu forger :

<s:Envelope 
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ObjectID>0/0</ObjectID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>id,dc:title,res,sec:CaptionInfo,
sec:CaptionInfoEx,pv:subtitlefile</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>0</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>

La partie structurante de cette requête est :

<ObjectID>0/0</ObjectID>

qui définit le chemin dans lequel je cherche les ressources UPnP : 0/0.
(j’ai déjà déterminé que le chemin de « Freebox TV » était au format « 0/0/XX », alors autant en profiter)

Un petit curl permettra alors de tester cette requête :

 curl -XPOST "http://192.168.0.254:52424/service/ContentDirectory/control" -d '...'

(où les « … » sont à remplacer par la requête SOAP)

Traitement de la réponse

La réponse aura l’allure suivante :

<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:BrowseResponse 
xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<Result>
...
</Result>
<NumberReturned>4</NumberReturned>
<TotalMatches>4</TotalMatches>
<UpdateID>0</UpdateID>
</u:BrowseResponse>
</s:Body>
</s:Envelope>

Les données présentes dans la balise Result constituent la réponse en elle-même. Elle doit être à son tour désérialisée en XML :

<DIDL-Lite 
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" 
xmlns:dc="http://purl.org/dc/elements/1.1/" 
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" 
xmlns:sec="http://www.sec.co.kr/" 
xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">
<container id="0/0/86" parentID="0/0" restricted="1">
<dc:title>Freebox TV</dc:title>
<upnp:class>object.container.storageFolder</upnp:class>
</container>
<container>...</container>
...
</DIDL-Lite>

YESSS !!! On y est : l’information est là :

  • title: Freebox TV
  • container id : 0/0/86

On doit donc rechercher le titre ‘Freebox TV’ pour trouver l’ID : ‘0/0/86’ qui est le chemin initialement recherché. Win !

Hop, on assemble tout ça !

Maintenant, il faut scripter tout ça pour mettre à jour mes paramètres.

Pour ce genre de job, plusieurs possibilités me sautent à l’esprit.

Pourquoi pas un script shell ? Non, trop fouilli à maintenir et vraiment pas idéal pour traiter du XML.

Un script Groovy ? C’est mon premier réflexe pour ce type de traitement : flexible avec une API riche. Problème : la plateforme. Si on tourne sur Raspberry Pi, il faut que ça ‘poutre’. On ne peut pas se permettre de monter une JVM en mémoire à chaque démarrage.

Dernière possibilité qui me saute à l’esprit : un programme Go. Des performances dignes du C avec une API très riche et bien documentée. Banco !

Le résultat

Je ne vais pas rentrer dans le détail du programme Go, juste quelques éléments de réponse.

Le code du programme est disponible sur github : https://github.com/gcastel/go-snippets/blob/master/upnp/findFbxUpnpPath.go

Le requêtage s’effectue grâce à l’API http.NewRequest :

req, err := http.NewRequest("POST", url, bytes.NewBuffer(soap_data))
req.Header.Set("SOAPACTION", "urn:schemas-upnp-org:service:ContentDirectory:1#Browse")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
      panic(err)
}
defer resp.Body.Close()

body, _ := ioutil.ReadAll(resp.Body)

L’unmarshalling XML nécessite de définir les structures à accéder (avec leurs tags). Exemple :

type Container struct {
        XMLName xml.Name `xml:"container"`
        Title string `xml:"title"`
        Class string `xml:"class"`
        Id string `xml:"id,attr"`
}

Le parsing/unmarshalling fonctionne alors avec l’API xml.Unmarshal :

var d DIDLLite
var id string
xml.Unmarshal([]byte(response), &d)
for _,container := range d.Containers {
   if container.Title == "Freebox TV" {
     id = container.Id
   }
}

Et voilà, on en est venu à bout.
J’espère que quelques uns de ces éléments pourront être utiles 😀

Java 8 sur Raspberry Pi : No soucy !

Je continue mes expériences avec la JVM sur Raspberry Pi : attaquons-nous au JDK 8.

Vous pouvez télécharger le « JDK 8 (with JavaFX) for ARM Early Access » à l’adresse suivante : http://jdk8.java.net/fxarmpreview/
Update : Un nouveau build est disponible ici : http://jdk8.java.net/download.html

Dézippez le tar.gz obtenu dans le répertoire de votre choix. Exemple :

cd lerepertoiredemonchoix
tar zxvf jdk-8-ea-b36e-linux-arm-hflt-29_nov_2012.tar.gz

Ensuite, une solution simple pour utiliser la VM est de faire pointer la variable d’environnement JAVA_HOME sur ce répertoire et d’ajouter le chemin $JAVA_HOME/bin à la variable PATH.

Mais on peut mieux faire : utiliser jenv

Jenv
Jenv va nous permettre de gérer plusieurs environnements d’exécution Java en fonction du répertoire d’où la JVM est lancée.
Pour l’installer, il suffit de suivre les instructions du site :

$ git clone https://github.com/hikage/jenv.git ~/.jenv
$ echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(jenv init -)"' >> ~/.bash_profile
$ exec $SHELL -l

Il faut maintenant paramétrer les JVMs disponibles, ex :

$ jenv add /usr/lib/jvm/java-7-openjdk
openjdk32-1.7.0.17 added
$ jenv add /path/to/jdk1.8.0
oracle32-1.8.0-ea added

Reste maintenant à définir les JDKs à utiliser en fonction du répertoire.
Tout d’abord, définissons le JDK global :

$ jenv global oracle32-1.8.0-ea

Autant utiliser le JDK 1.8 partout (si on peut ainsi envoyer des rapports de bugs et s’assurer de la compatibilité de nos applications, c’est toujours ça de pris !).

Pour les applications nécessitant java 7, pas de problèmes :

$ cd uneapplicationjava7
$ jenv local openjdk32-1.7.0.17

Bon, on teste ?
En global :

$ cd
$ java -version
java version "1.8.0-ea"
Java(TM) SE Runtime Environment (build 1.8.0-ea-b36e)
Java HotSpot(TM) Client VM (build 25.0-b04, mixed mode)

et en local :

$ cd uneapplicationjava7
$ java -version
java version "1.7.0_17"
OpenJDK Runtime Environment (IcedTea7 2.3.8) (ArchLinux build 7.u17_2.3.8-1-arm)
OpenJDK Zero VM (build 22.0-b10, mixed mode)

Et voilà, vous avez un java 8 utilisable à côté de votre java 7.

Note: Si vous obtenez une erreur « java: No such file or directory » à l’exécution de « java -version », exécutez la commande suivante en root :

ln /lib/ld-linux.so.3 /lib/ld-linux-armhf.so.3

Accéder à ses périphériques USB avec VirtualBox sous Linux

Voici une petite astuce qui m’a servie récemment pour utiliser virtualbox sous ubuntu.

Lors de l’accès au panneau de configuration des périphériques usb, impossible d’activer quoi que ce soit.

Il s’agit d’un problème de droits d’accès très classique, voici la solution !

Editez le fichier :

/etc/fstab

et ajoutez les lignes suivantes en fin de fichier :

#Activation de l’USB avec Virtualbox
none /proc/bus/usb usbfs devgid=46,devmode=664 0 0

Remontez vos périphériques (pour que les nouveaux droits du filesystem usb soient pris en compte) :

sudo mount -a

Lancez VirtualBox, et vous devriez pouvoir accéder à la page de configuration des périphériques USB.

[ How to Get USB Working in Virtual Box on Ubuntu Intrepid Ibex ]

Faire un rsync sur des répertoires locaux

Une petite news rapide pour partager une astuce bien pratique.

Pour faire des backups, une solution simple est de répliquer le contenu d’un répertoire sur un disque dur dédié. Pour ne pas recopier systématiquement les mêmes fichiers mais juste les différences, un utilitaire idéal existe : rsync.

Si vous souhaitez répliquer le répertoire /home/toto/src vers /mnt/backup/dest la ligne de commande suivante fera parfaitement l’affaire:


rsync -vur --delete /home/toto/src/ /mnt/backup/dest/

[ Yes, you can rsync between two local directories ]