Un répondeur multilingue avec Asterisk

Ayant de la famille à l'étranger, je me souviens d'un incident où des membres de ma famille avaient tenté de m'appeler sur mon portable et qu'ils n'avaient pas compris qu'ils étaient tombés sur mon répondeur, probablement du fait de mon annonce en français.

C'est pourquoi j'ai toujours pensé qu'un répondeur qui sache diffuser son annonce dans la bonne langue, en la dérivant du numéro de l'appelant, serait fort appréciable.

Un tel service va généralement au-delà de ce que fournit un opérateur téléphonique. Je me suis donc dit que ce serait là un bon exercice pour mettre en place un serveur de téléphonie Asterisk. Ce faisant, cela me permettrait également de pallier d'autres lacunes de mon propre équipement ou tout simplement réaliser des applications plus ludiques plus tard.

Présentation d'Asterisk

On présente souvent Asterisk comme étant un IPBX, c'est-à-dire un autocommutateur téléphonique privé (aussi appelé PBX) qui fonctionne en voix sur IP. On trouve beaucoup d'autocommutateurs dans des entreprises ou institutions publiques : lorsqu'on appelle un numéro et qu'on entend une voix robotisée demander un numéro de poste, ou qu'on se retrouve face à un menu, c'est le PBX qui "répond".

Le volet "voix sur IP", quant à lui, signifie grosso modo qu'on peut utiliser Asterisk pour faire son Skype interne dont les communications ne passent pas par Microsoft (et probablement pas la NSA non plus mais je ne m'avancerai pas là-dessus, on sait jamais), tout en permettant les appels vers l'extérieur et l'utilisation aussi bien de téléphones analogiques (via les fameuses cartes dites "FXO/FXS", comme l'Openvox A400P) que des téléphones SIP (comme les Cisco qu'on voit à peu près partout) ou encore des logiciels comme Ekiga ou Linphone.

On peut donc utiliser Asterisk pour passer des appels externes depuis un PC muni d'un casque et d'un micro, à l'aide d'un logiciel appelé "softphone", ce qui est pratique lorsqu'on est amené à prendre beaucoup de notes pendant la conversation. Mais on peut aussi brancher directement un téléphone analogique dessus, faire sonner des softphones depuis celui-ci, ou faire sonner le téléphone analogique en composant des numéros internes.

L'architecture matérielle

Pour ce faire, nous partirons du principe qu'on dispose déjà d'un serveur et d'une carte FXO/FXS avec au moins :

  • 1 module FXS sur le port 1 (pour brancher un téléphone dessus et pouvoir le faire sonner), éventuellement plusieurs sur les ports 2 et 3 le cas échéant;
  • 1 module FXO sur le port 4, qui ira dans la prise FXS ("téléphone") de sa box ADSL ou directement dans le mur.

Enfin, nous nous munirons également d'un bon vieux téléphone analogique (c.à.d. conçus pour se brancher sur une prise en T ou sur une prise RJ11 murale et non pas sur un réseau Ethernet). Pas que ce soit forcément nécessaire, mais comme je disposais déjà d'un téléphone avant de migrer vers Asterisk, ce serait dommage de ne pas s'en servir.

Le plan de numérotation

Installer Asterisk implique déjà de réfléchir à un plan de numérotation. Même si ce n'est pas forcément obligatoire dans notre cas, cela facilitera grandement les tests. Chaque terminal susceptible de se connecter au serveur Asterisk (dans notre cas, un téléphone analogique) doit donc disposer d'un numéro.

Dans mon cas, comme la plage des numéros à 4 chiffres commençant par 4 n'est pas utilisée par France Télécom et que je souhaite que le reste des numéros composés soit redirigée vers l'extérieur (pas de 9 ou de 0 à composer avant d'obtenir une ligne externe), j'adopterai le plan suivant :

  • 4001 pour le téléphone analogique;
  • 4002 à 4099 pour d'autres téléphones, éventuellement ;
  • 4100 pour écouter ses messages vocaux ;
  • n'importe quelle autre plage de numéros valable et attribuée par l'ARCEP sera redirigée vers l'extérieur.

Ce plan donne donc le fichier extensions.conf suivant :

[trunkint]
;
; Appels téléphoniques internationaux
;
exten => _00.,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})

[trunknorm]

; Appels téléphoniques "locaux"
exten => _0[12345679]XXXXXXXX,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})
exten => _3XXX,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})

; Numéros Azur (prix appel local)
exten => _081XXXXXXX,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})

[trunknumverts]

; Numéros verts
exten => _080XXXXXXX,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})
exten => _10XX,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})

[trunknumsurtax]

; Numéros surtaxés
exten => _08[289]XXXXXXX,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})

; Services de renseignements à la con
exten => _118XXX,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})

[trunknumurg]
; SAMU
exten => 15,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})
; Police/gendarmerie
exten => 17,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})
; Pompiers
exten => 18,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})
; Numéro d'urgence européen
exten => 112,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})
; Personnes sourdes ou malentendantes
exten => 114,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})
; Urgences sociales ("SAMU social")
exten => 115,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})
; Enfants disparus
exten => 116000,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})
; Enfance maltraitée
exten => 119,1,Dial(${GLOBAL(OUT_PSTN)}/${EXTEN})

[phones]

include => internal

include => trunknorm
include => trunknumurg
include => trunknumsurtax
include => trunknumverts
include => trunkint


[internal]

exten => 4001,1,Dial(DAHDI/1,25,x)      ; téléphone analogique
  same => n,VoiceMail(${EXTEN},s)
  same => 101,VoiceMail(${EXTEN},s)

exten => 4100,1,NoOp()
  same => n,VoiceMailMain(${CALLERID(num)})

On déclare donc tout d'abord tous les numéros de téléphone pour lesquels on souhaite relayer l'appel vers l'extérieur. Il s'agit bien entendu de tous les numéros en zéro-quelque-chose à 10 chiffres, mais aussi tous les numéros courts en 4 chiffres (seuls ceux commençant par 1 et 3 sont attribués) et, très important, les numéros d'urgence.

La configuration du canal analogique

Il faut également configurer le fichier chan_dahdi.conf avec au moins les directives suivantes :

[channels]

signalling=fxo_ks
callerid="Votre nom ici" <4001>
mailbox=4001
group=0
context=phones
language=fr
channel => 1


signalling=fxs_ks
callerid=asreceived
group=1
context=incoming
busydetect=yes
busycount=3
busypattern=500,500
language=fr
channel => 4

Cette configuration est normalement en partie auto-générée, mais j'ai ajouté quelques éléments dans le bloc correspondant au "channel" 4 (i.e. le port 4, celui branché sur l'extérieur). J'ai ajouté busydetect=yes, busycount=3 et busypattern=500,500 parce que sinon, la carte ne sait pas quand l'interlocuteur a raccroché (ma Livebox ne fournit pas de signal spécifique lorsque la ligne a été libérée, en tout cas).

Enfin, pour que la suppression d'écho se fasse correctement, il faut charger le module wctdm (ou celui nécessaire pour votre carte) avec l'option opermode=TBR21 pour régler l'impédance de ligne à la valeur adaptée pour la France. Il suffit d'ajouter dans /etc/modprobe.d/dahdi.conf la ligne :

options wctdm opermode=TBR21

Redémarrer la machine, ou taper rmmod wctdm; modprobe wctdm en tant que root.

Les boîtes vocales

Avant de continuer, il faut savoir qu'Asterisk ne fournit pas automatiquement une boîte vocale par utilisateur ou numéro de téléphone, mais qu'il faut les configurer explicitement. Dans notre cas, on va simplement mettre en place une messagerie vocale pour le 4001. Il suffit d'ajouter dans voicemail.conf :

[default]
4001 => 1337,Téléphone,vous@example.com

Ceci crée une boîte vocale pour le 4001, avec comme mot de passe 1337 et comme nom d'utilisateur "Téléphone". Les e-mails seront envoyés à vous@example.com. Tant qu'on y est, modifions la variable emailbody pour que le message soit en français (oui c'est sur une seule ligne) :

emailbody=${VM_NAME},\n\n${VM_CALLERID} vous a ${IF($["${VM_CIDNUM}" =
"${ORIG_VM_CIDNUM}"]?laissé:transféré)} un message de longueur ${VM_DUR}\ndans
votre boîte vocale ${VM_MAILBOX} le ${VM_DATE}.\n${IF($["${VM_CIDNUM}" =
"${ORIG_VM_CIDNUM}"]?:Ce message a été laissé par ${ORIG_VM_CALLERID} le
${ORIG_VM_DATE}.)}\nVous pouvez l'écouter en appelant le 4100 depuis un
téléphone de l'appartement ou en téléchargeant\nla pièce jointe.\n

Les appels entrants

Cette configuration permet d'émettre des appels depuis un téléphone interne, mais pas d'en recevoir !

Comme on peut le voir, décrocher son téléphone analogique le place dans le contexte "phones" depuis lequel on peut appeler les numéros externes et internes. Cependant, un signal de sonnerie de l'extérieur place l'appelant dans le contexte "incoming", que nous n'avons pas encore paramétré.

L'idée principale étant que tout appel arrivant dans le contexte incoming sera simplement relayé vers le téléphone analogique. C'est là-dedans que nous allons faire le changement de langue.

On en profite tout d'abord pour envoyer bouler les appels masqués avec PrivacyManager, parce qu'il s'agit généralement de personnes indésirables. Cette application invite l'interlocuteur à composer son propre numéro de téléphone, qui sera ensuite affiché sur les téléphones lorsqu'ils sonnent. Une mesure temporaire, car j'envisage de remplacer cela par une invite qui demande à la personne de s'identifier oralement.

Le principe est simple : tout ce qui a "0031" comme 4 premiers caractères vient des Pays-Bas, et on change donc la langue en néerlandais ; tout ce qui commence par 0049 (Royaume-Uni) ou 001 (États-Unis/Canada) déclenche un changement de langue en anglais (y compris le Québec, mais je vous laisserai trouver les indicatifs correspondants vous-mêmes). Dans le cas contraire, on laisse en français.

On fait sonner le téléphone sur le port 1 de la carte pendant 25 secondes, puis on passe au répondeur.

[incoming] 

include => internal

exten => s,1,PrivacyManager()
  same => 2,Set(MYLANGUAGE=DEFAULT)
  same => 3,Set(MYLANGUAGE=${IF($[ ${CALLERID(num):0:4} = 0031 ] ? nl)})
  same => 4,Set(MYLANGUAGE=${IF($[ ${CALLERID(num):0:4} = 0049 ] ? en)})
  same => 5,Set(MYLANGUAGE=${IF($[ ${CALLERID(num):0:3} = 001 ] ? en)})
  same => 6,Set(MYLANGUAGE=${IF($[ ${MYLANGUAGE} = DEFAULT ]) ? fr})
  same => 7,Set(CHANNEL(language)=${MYLANGUAGE})
  same => 8(privmgr-passed),NoOp(${CHANNEL(language)})
  same => 9,Dial(DAHDI/1,25,x)
  same => 10,Playback(cust-vm-intro)
  same => 11,VoiceMail(4001,s)
  same => 12,Playback(vm-nobodyavail)
  same => 13,Hangup()
  same => 109,Playback(cust-vm-intro)
  same => 110,VoiceMail(4001,s)
  same => 111,Hangup()

Il suffit ensuite d'enregistrer deux exemplaires de l'annonce répondeur, en WAV 16 bits mono 8 kHz, et de les placer dans /usr/share/asterisk/sounds/<LANGUE>/cust-vm-intro.wav. Normalement, cela suffit. L'option s de VoiceMail() désactive l'annonce par défaut pour le répondeur et laisse uniquement le bip sonore.

Pour enregistrer ses annonces vocales, le plus simple est de mettre en place un numéro "virtuel" dans le contexte internal, qui active la boîte vocale du téléphone analogique. Il suffit d'enregistrer une annonce par ce biais et récupérer le message envoyé par e-mail :

exten => 4102,VoiceMail(4001,s)

Ainsi, en composant le 4102 depuis le téléphone, on entendra juste un bip sonore, puis à vous d'enregistrer les annonces pour votre répondeur, un par un. Une fois que vous avez récupéré les pièces jointes, composez le 4100 pour accéder à votre répondeur et supprimer les annonces que vous venez d'enregistrer.

Une fois que tout est en place, il suffit tout simplement de relancer Asterisk.

Le résultat final est qu'en cas de non-réponse, l'annonce cust-vm-intro est lue dans la bonne langue, puis l'appelant entendra un bip et pourra enregistrer son message. Si, au contraire, on tombe sur la messagerie vocale de la Livebox par exemple, il faut régler celui-ci pour que le répondeur se déclenche au bout de 9 sonneries (au lieu de 5), et s'arranger pour qu'Asterisk réponde avant.

Et en bonus, on reçoit également un exemplaire du message vocal par e-mail, en pièce jointe, ce qui est totalement la classe.

Conclusion

Tout cela paraît peut-être un peu tordu juste pour ajouter une seule fonctionnalité, mais on peut imaginer beaucoup d'autres possibilités. Le fait que le langage des plans de numérotation soit Turing-complet permet à n'importe qui de mettre en place son système téléphonique avec ses besoins spécifiques. Ce projet ne m'aura coûté que 220 euros pour la carte FXO/FXS, ce qui paraît un peu cher à première vue, mais reste tout à fait respectable pour le service que cela rend.

Il y a peut-être des choses que je pourrais améliorer dans mon plan de numérotation, mais j'ai essayé de vous montrer le strict minimum nécessaire pour utiliser une ligne téléphonique analogique avec Asterisk.

Comme ma ligne est derrière une Livebox, une idée connexe consisterait à laisser Asterisk s'enregistrer lui-même auprès du serveur de téléphonie Orange ; après tout, j'ai déjà pu voir comment ça marche. Ceci serait fort appréciable pour ceux qui souhaiteraient utiliser leur propre matériel au lieu de leur Livebox tout en conservant le téléphone.

Enfin, pour plus d'informations sur comment fonctionne Asterisk ou le téléphone, je vous conseille de lire l'excellent Asterisk™: The Future of Telephony disponible en PDF.

Posté par x0r à 2 commentaires • Tags : sip voip asterisk telephone fxo fxs

Surveiller sa Livebox 3 avec Nagios

Ça fait quelques temps déjà que je me suis mis en tête de surveiller ma propre petite infrastructure réseau. Avec mon serveur chez moi qui fournit le NFS, le DNS, pas mal de petits sites Web et tout plein d'autres choses, ma jail FreeBSD qui héberge ce site ainsi que monrer.fr, mon routeur et mes onduleurs, on n'a pas forcément toujours le temps de vérifier soi-même si tout marche, alors que certaines choses feraient mieux de ne pas être cassées trop longtemps.

Pour ce faire, j'ai choisi Nagios, comme beaucoup de monde le ferait. Mais je me suis rapidement rendu compte qu'il me manquait quelque chose : en effet, bien que je surveille ma Livebox, il lui arrive parfois de répondre au ping alors que la connexion Internet ne fonctionne pas (lors d'un renouvellement d'un bail DHCP par exemple, ou pour toute autre raison). Je voulais donc pouvoir distinguer le cas où ma jail FreeBSD est dans les choux du cas où c'est la connexion Internet elle-même qui ne marche plus.

C'est pourquoi j'ai codé quelques plugins Nagios que vous pourrez récupérer sur mon dépôt BitBucket. Ils se connectent tous sur l'interface d'administration de la Livebox pour récupérer l'état de la connexion Internet, mais aussi du téléphone et de la télévision.

Cet article s'applique uniquement à la Livebox 3, qui est le dernier modèle au moment où j'écris cet article et qui est fournie à chaque nouvel abonnement fibre chez Orange.

L'interface d'administration : du HTML mais pas de texte

Tout d'abord, si vous allez sur l'interface d'administration de votre Livebox (http://192.168.1.1 par défaut) et que vous regardez le code source de la page, vous trouverez beaucoup de balises <span> mais pas un seul mot de texte. Par exemple, la partie qui nous intéresse pour l'état de la connexion Internet est (moyennant le whitespace ajouté pour plus de clarté) :

<span class="translation Translations.general.label.internet"></span>
<br>
<span style="color:#3caa0a;"
    class="translation Translations.general.label.available display"
    id="Services.InternetConnection"
    name="Services.InternetConnection">
</span>

Ce bout de HTML est censé donner ça :

État des services de la Livebox

Il s'agit donc bien de la partie de l'interface qui affiche une icône verte et le texte « disponible ». En fait, la page contient un bout de JavaScript qui insère le texte dans la bonne langue au bon endroit (au lieu de tout simplement honorer l'en-tête Accept-Language du protocole HTTP). C'est pas très élégant, mais cela nous permettra au moins de nous affranchir de problèmes liés aux différentes langues dans lesquelles les Livebox seraient fournies.

Donc pour obtenir la disponibilité de la connexion Internet, il suffit de regarder l'attribut class de l'élément HTML dont l'identifiant est Services.InternetConnection.

L'état de la TV et du téléphone : AJAX à la rescousse

La première version de mon script se contentait tout simplement de récupérer cette page HTML de temps en temps. Cependant, un petit coup de Firebug m'a rapidement montré que l'état des services TV et téléphone sont récupérés par des appels AJAX et ne sont pas affichés directement sur la page HTML.

Sous peine de fausses alertes Nagios (le HTML indiquant que la TV et le téléphone sont down par défaut), il nous faut donc faire un peu plus de travail pour récupérer l'état de ces services.

Pour l'état de la téléphonie, c'est à l'URL suivante que ça se passe :

http://192.168.1.1/sysbus/VoiceService/VoiceApplication:listTrunks

Il suffit d'envoyer une requête POST avec comme données {"parameters":{}} (un GET ne suffit pas et renverrait une erreur), et on obtient une réponse qui renferme en sus beaucoup d'informations intéressantes :

{
  "status": [
    {
      "name": "SIP-Trunk",
      "signalingProtocol": "SIP",
      "enable": "Enabled",
      "trunk_lines": [
        {
          "name": "LINE1",
          "groupId": "Group1",
          "enable": "Enabled",
          "status": "Up",
          "statusInfo": "",
          "directoryNumber": "+33123456789",
          "uri": "+33123456789@orange-multimedia.fr",
          "authUserName": "CENSURAIDE@orange-multimedia.fr",
          "authPassword": "",
          "event_subscribe_lines": [
            {
              "eventSubscribeEvent": "message-summary",
              "eventSubscribeAuthUserName": "",
              "eventSubscribeAuthPassword": ""
            }
          ]
        }
      ],
      "sip": {
        "proxyServer": "",
        "proxyServerPort": 5060,
        "registrarServer": "",
        "registrarServerPort": 5060,
        "outboundProxyServer": "81.253.172.17",
        "outboundProxyServerPort": 5060,
        "userAgentDomain": "orange-multimedia.fr",
        "userAgentPort": 5060,
        "subscriptionInfo": [
          {
            "event": "message-summary",
            "notifyServer": "voicemail.orange-multimedia.fr",
            "notifyServerPort": 5060
          }
        ]
      },
      "h323": {

      }
    },
    {
      "name": "H323-Trunk",
      "signalingProtocol": "H.323",
      "enable": "Disabled",
      "trunk_lines": [
        {
          "name": "LINE3",
          "groupId": "Group1",
          "enable": "Disabled",
          "status": "Disabled",
          "statusInfo": "",
          "directoryNumber": "",
          "uri": "",
          "authUserName": "",
          "authPassword": "",
          "event_subscribe_lines": [

          ]
        }
      ],
      "sip": {

      },
      "h323": {
        "gatekeeper": "",
        "gatekeeperPort": 1719
      }
    }
  ]
}

Le numéro de téléphone des attributs directoryNumber et uri est bien entendu le numéro de téléphone de votre Livebox. En tout cas, cela montre que ma Livebox utilise le protocole SIP.

Le paramètre authUserName me semble un peu plus abscons cependant. Il s'agit en réalité d'un identifiant de 10 caractères composé uniquement de lettres majuscules et qui ne correspond avec aucun des identifiants qu'Orange m'a fournis.

Hélas, je ne parviens pas à trouver plus d'informations concernant la possibilité de bénéficier de son accès VoIP avec autre chose qu'une Livebox. Peut-être faudra-t-il que je fasse un jour l'expérience en sniffant les communications entre la Livebox et le GPON sur le bon VLAN. De toute façon, là n'est pas l'objet de l'article. (Edit : ça, c'est fait).

Passons à l'état de la télévision. Il y a un peu moins de choses rigolotes à glaner dans le JSON renvoyé par l'URL correspondante, mais en POSTant {"parameters":{}} sur l'URL http://192.168.1.1/sysbus/NMC/OrangeTV:getIPTVStatus, on obtient :

{"status":null,"data":{"IPTVStatus":"Available"}}

Simple et efficace.

Conclusion

Cela fait quelques semaines que j'ai ajouté ces plugins Nagios à mon service de monitoring, mais je n'ai pas encore eu d'alertes à propos d'éventuelles coupures de service.

En tout cas, si vous aussi voulez vous laisser tenter, vous pouvez récupérer le code source sur la page BitBucket correspondante. Il suffit de copier les scripts dans /usr/lib/nagios/plugins (ou là où sont les autres plugins Nagios sur votre système), ajouter les commandes correspondantes et c'est parti.

Posté par x0r à 6 commentaires • Tags : orange livebox nagios monitoring surveillance alerte

Réouverture (progressive) de monrer.fr

Ça m'aura pris un peu plus longtemps que je le pensais, mais je vais enfin pouvoir rouvrir monrer.fr !

Comme cela faisait quand même presque 6 semaines de downtime, je pense que je vous dois quand même quelques explications.

Dans mon post précédent, j'avais expliqué comment j'avais fait de la rétroingénierie sur l'appli Transilien officielle pour Android (attention, vidéo en auto-play sur la page) afin de déterminer comment cette appli faisait pour récupérer les horaires en live des prochains trains de chaque gare. L'étape suivante était bien entendu d'implémenter le fruit de mes trouvailles. Et comme vous pouvez le constater, c'est plus ou moins pour cette raison que j'ai eu quelques ennuis.

Une alerte Nagios plus tard, je me suis rendu compte que le serveur qui héberge monrer.fr s'est soudainement fait bannir. Je ne pouvais donc plus récupérer les horaires temps réel des trains.

Après quelques échanges avec plusieurs employés de la SNCF qui ont pris l'initiative de me contacter (et que je salue au passage), je me suis mis en tête d'essayer l'API Open Data officielle que la SNCF fournit depuis quelques mois, et qui est (d'un point de vue fonctionnel) grosso modo équivalent aux sources de données que j'utilisais avant. Cette nouvelle source de données n'est pas encore tout à fait parfaite (en particulier, seules les gares des lignes C et L sont interrogeables), mais que l'ouverture du service à l'ensemble des gares Transilien serait pour bientôt. Cette restriction sur les gares interrogeables se reflète pour le moment dans monrer.fr, mais dès que j'aurai des nouvelles, je vous tiendrai au courant.

Les changements d'un point de vue technique

J'ai profité du downtime pour retaper une bonne partie du code qui s'occupait de la récupération des informations proprement dite, pour me permettre à l'avenir de confronter plus facilement plusieurs sources de données. Je le fais d'ailleurs déjà, car pour un train donné, je récupère son horaire temps réel via l'API et son horaire théorique à l'aide des horaires Transilien en GTFS pour calculer son retard.

J'ai aussi dû retaper la gestion des gares de manière générale, parce que toutes mes sources de données utilisent les codes UIC en interne alors que je m'étais longtemps fié aux codes TR3.

Sinon, le site sait désormais par quelles lignes sont desservies chaque gare. Ça se voit dans les pop-ups d'autocomplétion :

Nouvelle autocomplétion sur monrer.fr

Cette fonction paraît anodine mais cela me permettra au moins de poser les bases pour une feature request que j'ai eue, à savoir pouvoir filtrer les résultats par ligne.

Conclusion

J'ai attendu assez longtemps avant de rouvrir, parce que je voulais être sûr de ne pas laisser trop de bugs dans ce que j'allais faire. Cependant, tout n'est pas encore parfait, et il se peut que vous tombiez sur l'un des messages d'erreur suivants :

  • API is broken : erreur interne du serveur qui fournit l'API Prochains Départs. Suffit de rafraîchir la page et ça devrait repartir.

  • API call quota exceeded : là c'est parce que vous êtes trop nombreux. J'ai pour le moment toujours le quota à 10 appels par minute (ce qui me permet d'avoir 2 users regardant des gares différentes au maximum à un instant donné), mais heureusement pas pour longtemps.

Sinon, s'il y a autre chose, vous pouvez vous exprimer dans les commentaires, m'envoyer un mail ou jeter un œil au bugtracker du site.

Posté par x0r à 12 commentaires • Tags : monrer transilien sncf gtfs train rer-web

Les fonctions variadiques en C

Jusqu'à maintenant, tout programmeur débutant en C et ayant écrit ses propres fonctions sait qu'une fonction peut prendre un certain nombre de paramètres.

Chacune de ces fonctions ont un nombre fixe de paramètres : une fonction qui prend trois paramètres doit forcément être appelée en renseignant ces trois paramètres.

Cependant, il y a des fonctions qui semblent échapper à la règle et qui autorisent un nombre variable de paramètres. L'un d'entre eux est probablement la plus utilisée en C : il s'agit de printf. On peut donner autant d'arguments qu'on veut à printf, tant qu'on respecte les règles de syntaxe dans la chaîne de format (le premier argument à printf). Ces fonctions sont appelées fonctions variadiques pour cette raison.

On peut tout à fait écrire

printf("Bonjour le monde\n");

ou encore

printf("La réponse est %d", 42);

ou encore

printf("J'ai deux chats : %s et %s", "Mélusine", "Laptite");

Si printf le fait, alors pourquoi pas nous ? En fait, rien ne nous empêche d'écrire nos propres fonctions variadiques. Je vais donc vous montrer dans ce post comment on écrit de telles fonctions, comment on exploite un nombre variable d'arguments et quelles sont les précautions à prendre.

Fonctions variadiques, mode d'emploi

Le C est un langage qui a plus de 40 ans et la gestion des arguments variables a été standardisée dans le C89 (la fonction va_copy() a ensuite été ajoutée dans le C99). C'est pourquoi nous devons d'abord examiner les limitations de cette technique avant de commencer.

Limitations techniques

Tout d'abord, il faut savoir qu'une fonction variadique doit obligatoirement accepter au moins un argument fixe. On ne peut pas définir de fonction variadique qui puisse accepter zéro paramètre, c'est-à-dire que le code suivant ne fonctionnera pas :

void ma_fonction(...);

Il n'existe pas non plus de mécanisme qui permette de savoir si on a fini de lire la liste des arguments, ou qui permette de savoir combien d'arguments ont été passés. Il faut alors choisir l'une des deux solutions :

  • on donne un paramètre qui indique le nombre de paramètres qui suivent, un peu comme le paramètre argc de main() qui donne la taille du tableau argv ;
  • ou alors on exige de terminer la liste d'arguments par une valeur spéciale (une "sentinelle"), telle que NULL (possible uniquement lorsque la fonction prend comme argument des pointeurs).

L'une de ces deux solutions peut ne pas être possible à mettre en œuvre pour des raisons diverses et variées. D'ailleurs, dans le cas de printf, on part du principe qu'il y a autant d'arguments qui suivent la chaîne de format que de %s, %d ou autres dans cette chaîne, donc il peut y avoir d'autres solutions que les deux ci-dessus.

Enfin, bien que nous ne soyions pas obligés d'utiliser les mêmes types pour chacun des arguments d'une fonction variadique (comme pour printf), rien ne nous permet de savoir à l'avance de quels types sont les arguments, et rien ne nous permet d'imposer des contraintes sur leurs types. C'est pour cette raison que ce code compile même s'il segfaulte :

printf("%d %s", "bonjour", 42);

Comme beaucoup de choses en C, ce mécanisme fournit très peu de garde-fous et les occasions de se planter sont nombreuses si on ne fait pas attention.

Un coup d'œil au man

Pour utiliser des fonctions variadiques, il faudra inclure le fichier stdarg.h dans son fichier C.

La lecture de la page de man de stdarg(3) (je n'ai pas trouvé de versions en français à jour, désolé) montre que la gestion des arguments dans une fonction variadique se fait avec quatre fonctions ou macros :

  • va_start(), qui nous permet de commencer à parcourir la liste d'arguments passés à notre fonction ;
  • va_arg(), qui nous permet de récupérer un argument de cette liste à chaque appel ;
  • va_end(), qui permet de signaler qu'on a fini de lire la liste d'arguments ;
  • enfin, va_copy() si on a besoin de copier des listes d'arguments pour une raison ou une autre.

Si vous avez une machine UNIX sous la main, tapez la commande man 3 stdarg et vous aurez le manuel sous vos yeux.

Démonstration : la liste de courses

Pour illustrer le mécanisme des fonctions variadiques, je vous propose d'utiliser comme exemple une fonction qui se contente d'imprimer à l'écran une liste de courses (donc des chaînes). Un appel à cette fonction :

courses("oeufs", "chocolat", "sucre", "farine", NULL);

devra donner la sortie suivante :

J'ai 4 choses à acheter : oeufs, chocolat, sucre et farine.

L'algorithme est relativement simple et consiste en deux étapes :

  • Parcourir la liste d'arguments pour compter le nombre d'arguments passés à la fonction (le NULL de la fin ne compte pas) ;
  • Parcourir à nouveau la liste d'arguments depuis le début pour afficher la liste de courses.

Voici donc le code, commenté de manière appropriée :

#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>


void courses(char* arg0, ...) 
{
    va_list ap;

    int i, nb_args;
    char* cur_arg = arg0;

    /* On n'est jamais trop prudent */
    if (arg0 == NULL) {
        printf("Je n'ai rien à acheter\n");
        return;
    }

    /* Commencer à parcourir la liste des paramètres.  La liste s'appelle ap
     * et le dernier argument fixe est arg0.  */
    va_start(ap, arg0);

    /* Ajouter 1 à nb_args jusqu'à ce qu'on lise un argument de type char*
     * égal à NULL.
     * Cette boucle compte le nombre d'arguments y compris le NULL, mais comme
     * on "oublie" de compter arg0, pas besoin de soustraire 1 du résultat 
     * final. */
    for (
        nb_args = 0; 
        cur_arg != NULL;
        nb_args++, cur_arg = va_arg(ap, char*)
    ); 

    /* On a fini (pour cette fois) */
    va_end(ap);

    printf("J'ai %d %s à acheter : ", nb_args, 
            (nb_args > 1) ? "choses" : "chose");

    /* Reparcourir à nouveau la liste de paramètres et les afficher un à un.
     * Remarquez que la boucle commence à 1 car le premier argument lu avec va_arg()
     * est en réalité le *deuxième* ingrédient de notre liste de courses. */
    printf("%s", arg0);
    va_start(ap, arg0);
    for (i = 1; i < nb_args; i++) {
        if (i == nb_args - 1)
            printf(" et ");
        else
            printf(", ");
        printf("%s", va_arg(ap, char*));
    }
    printf("\n");

    va_end(ap);
}



int main(int argc, char* argv[])
{
    courses(NULL);
    courses("oeufs", NULL);
    courses("oeufs", "chocolat", NULL);
    courses("oeufs", "chocolat", "sucre", "farine", NULL);
    return 0;
}

Comme on peut le voir, nous sommes déjà obligés de contourner quelques-unes des limitations techniques de stdarg.h pour faire une fonction aussi simple. La première, c'est qu'on est obligés d'avoir un argument "fixe", que j'ai appelé arg0, pour dénoter le premier élément de notre liste de courses. Les autres éléments sont récupérés en utilisant la fonction va_arg(ap, char*). Ici, tous nos arguments sont de type char*. La deuxième, c'est qu'on a été obligé de compter les arguments avant de pouvoir les afficher.

En tout cas, c'est tout ce qu'il faut pour que ça marche, et lorsqu'on exécute le programme, on voit apparaître à l'écran :

Je n'ai rien à acheter
J'ai 1 chose à acheter : oeufs
J'ai 2 choses à acheter : oeufs et chocolat
J'ai 4 choses à acheter : oeufs, chocolat, sucre et farine  

Conclusion

En résumé, pour utiliser les fonctions variadiques en C, la recette est la suivante :

  • inclure stdarg.h dans son programme ;
  • déclarer une variable de type va_list ;
  • l'initialiser avec va_start() ;
  • lire les arguments un par un avec va_arg() ;
  • terminer avec va_end().

Cet exemple est très simple mais je suis sûr qu'il y a d'autres usages utiles pour ce genre de fonctions. Personnellement, je m'en sers le plus souvent pour faire un wrapper autour de printf (ou une fonction qui se comporte que la même manière que printf) dans des situations où on n'a pas directement accès à stdout et stderr (lorsqu'on réalise une application SDL en plein écran par exemple).

Posté par x0r à 4 commentaires • Tags : programmation c stdarg variadique variadic fonction argument variable

Utiliser des types opaques en C

Cela fait déjà quelques temps que je montre le langage C à ma chère et tendre Nausicaa et que je la vois faire de plus en plus de choses avec. Je me suis donc dit que je pourrais peut-être faire une série d'articles afin de l'aider à mieux coder, et les partager ici pour que vous en profitiez tous.

N'importe qui ayant fait du C de manière à peu près sérieuse a déjà utilisé des structs pour créer des types de données composites, par exemple des listes chaînées ou plus ou moins ce qu'on appellerait des classes dans des langages (orientés) objet.

Cependant, lorsqu'un programme C est constitué d'une multitude de modules et qu'il touche à de nombreux types de données, on rentre vite dans des problèmes de dépendances circulaires : un module a a besoin de la déclaration d'une struct type_b qui est dans le module b (le header b.h); or le module b a besoin de la déclaration d'un struct type_c dans le module c (fichier c.h), et ce module c a besoin de connaître l'existence du type struct type_a dans le module a (a.h). La compilation échoue alors sur une erreur de la forme « type struct type_a inconnu » alors qu'il est pourtant déclaré correctement.

Nous allons d'abord expliquer pourquoi ces dépendances circulaires n'en sont pas vraiment en expliquant la différence entre déclaration et définition ; nous verrons ensuite ce que sont les fameux types opaques et comment ceux-ci permettront de casser la majorité des dépendances circulaires.

La différence entre déclaration et définition

Imaginons que nous voulions mettre en place une structure de liste chaînée, très simple, et tant qu'on y est, générique. Alors, on va avoir deux fichiers, liste.h et liste.c. Un programmeur C novice écrirait alors quelque chose qui ressemblerait au listing suivant :

liste.h :

#ifndef HEADER_LISTE
#define HEADER_LISTE

typedef struct liste liste;

struct liste {
    void * data;
    liste* suivant;
};

liste* creation_liste();
liste* insertion(liste* l, void* data);

#endif

liste.c :

#include <stdlib.h>
#include "liste.h"

liste* creation_liste() {
    return NULL;
}

liste* insertion(liste* l, void* data) {
    liste* e;
    liste* new_element = malloc(sizeof(struct liste));
    new_element->data = data;
    new_element->suivant = NULL;

    if (l == NULL)
        return new_element;

    for (e = l; e->suivant != NULL; e = e->suivant);

    e->suivant = new_element;
    return l;
}

En C, il est très important de connaître la différence entre déclarer et définir un objet (que ce soit une fonction, une variable, un struct, enum, union ou typedef) :

  • Déclarer un objet, c'est signaler au compilateur qu'il existe. Par exemple, les prototypes des fonctions dans le .h sont des déclarations : on dit au compilateur « tiens, il y a une fonction creation_liste() et une fonction insertion() ; ce qu'ils font exactement est écrit ailleurs mais sache qu'ils existent vraiment » ;
  • Définir un objet, c'est expliciter au compilateur ce qu'est exactement cet objet. Si liste.h contient les déclarations de deux fonctions, on les définit dans le .c. Dans le cas de fonctions, on dit aussi qu'elles sont déclarées dans le .h et implémentées dans le .c.

La subtilité, c'est que lorsqu'on définit un objet qui n'a pas encore été déclaré ailleurs, on le déclare également. Par exemple, l'instruction

int a = 42;

et les deux instructions

int a;
a = 42;

font tous les deux une déclaration suivie d'une définition de la variable a.

Ajoutons maintenant un fichier main.c :

main.c :

#include <stdio.h>
#include "liste.h"

int main(int argc, char* argv[]) {
    liste* l = creation_liste();
    liste* e;

    int a = 42;
    int b = 1337;

    /* 
     * Normalement, ça c'est mal, parce que &a et &b ne sont plus
     * des pointeurs valables lorsqu'on sort de la fonction
     * main(), mais c'est juste pour la démonstration.
     */
    l = insertion(l, (void*)(&a));
    l = insertion(l, (void*)(&b));

    printf("Voici le contenu de ma liste : \n");
    for (e = l; e != NULL; e = e->suivant) {
        printf(" * %d\n", *(int*)(e->data));
    }

    return 0;
}

Ce fichier ajoute les entiers 42 et 1337 à une liste et les affiche aussitôt. Notre liste chaînée ne sait stocker que des pointeurs (les pointeurs void * sont des pointeurs génériques) ; il faut donc stocker les adresses de a et de b. Pour les afficher, on caste le void * en int * puis on déréférence le pointeur.

Passer aux types opaques

Ce genre de code fonctionne bien lorsqu'on a un petit projet. Cependant, il y a quelques problèmes potentiels :

  • si on décide de modifier la liste chaînée en liste doublement chaînée, ça casse les fonctions qui l'utilisent ;

  • si on décide de changer le nom des membres, ça casse toutes les fonctions qui les utilisent ;

  • chaque module utilisant liste.h peut casser les listes et peuvent par exemple modifier les pointeurs suivant alors que seules les fonctions dans liste.c ont besoin d'y toucher ;

  • enfin, chaque module utilisant notre liste chaînée a besoin de connaître la définition de la struct liste pour pouvoir fonctionner.

Le dernier point est le plus problématique, et c'est souvent la cause des dépendances circulaires. Heureusement, il est possible de séparer la déclaration et la définition de notre liste chaînée. Voilà comment on fait :

liste.h :

#ifndef HEADER_LISTE
#define HEADER_LISTE

typedef struct liste* liste_t;

liste_t creation_liste();
liste_t insertion(liste_t l, void* data);

#endif

La première modification majeure est qu'on a fait un typedef qui fait un alias vers un pointeur de struct liste. Ce typedef est parfaitement légal, car le compilateur connaît toujours la taille en mémoire d'un pointeur, indépendamment de ce vers quoi il pointe. Ainsi, un module qui inclut liste.h pour ensuite déclarer une liste_t maliste; quelque part dans une fonction ne fait pas d'erreurs à la compilation. De toute façon, nous ne faisions que manipuler des pointeurs vers ces structures tout au long.

On peut néanmoins se demander où est passée la définition de struct liste. Elle est dans le fichier .c correspondant :

liste.c :

#include <stdlib.h>
#include "liste.h"

struct liste {
    void * data;
    liste_t suivant;
};

liste_t creation_liste() {
    return NULL;
}

liste_t insertion(liste_t l, void* data) {
    liste* e;
    liste* new_element = malloc(sizeof(struct liste));
    new_element->data = data;
    new_element->suivant = NULL;

    if (l == NULL)
        return new_element;

    for (e = l; e->suivant != NULL; e = e->suivant);

    e->suivant = new_element;
    return l;
}

Là aussi on a remplacé toutes les occurrences de liste* par liste_t. À part ça, rien ne change.

Néanmoins, compiler le programme en l'état ne marchera pas immédiatement. gcc donnera des insultes du style Dereferencing pointer to incomplete type.

Ce que ça veut dire : en compilant main.c, on a inclus liste.h et le typedef struct liste* liste_t. Ainsi, gcc sait qu'un type liste_t existe et que c'est en fait un pointeur vers une struct liste. Mais comme on a caché la définition de la struct liste dans le .c, il ne connaît pas le contenu de la struct, ni sa taille, ni si le membre suivant existe et est bien du type liste_t, ni que pour accéder à ce membre suivant il faut en fait prendre les octets 4 à 7 de la struct (8 à 15 si vous êtes en 64 bits) et les interpréter comme un pointeur… bref, il ne connaît rien de tout cela.

Ce qui pose problème sont la troisième clause de la boucle for (donc e = e->suivant) et l'expression qui permet d'extraire une valeur de la liste, à savoir *(int*)(e->data).

La solution consiste tout simplement à remplacer ces clauses par deux fonctions : une fonction qui renvoie le membre element d'un liste_t et une autre fonction qui renvoie le membre data interprété en tant que int. Il suffit alors de les rajouter dans le header :

liste_t liste_get_suivant(liste_t element);
int liste_get_data_int(liste_t element);

et de les implémenter comme suit :

liste_t liste_get_suivant(liste_t element) {
    return element->suivant;
}

int liste_get_data_int(liste_t element) {
    return *(int*)(element->data);
}

Ce qui nous permet enfin de réécrire la boucle du main.c ainsi :

printf("Voici le contenu de ma liste : \n");
for (e = l; e != NULL; e = liste_get_suivant(e)) {
    printf(" * %d\n", liste_get_data_int(e));
}

Une petite macro pour finir

Si on ajoute dans list.h la macro suivante :

#define liste_foreach(iterator, list) \
    for ((iterator) = (list); \
        ((iterator) != NULL); \
        (iterator) = liste_get_suivant(iterator))

on peut alors écrire la boucle du main.c ainsi :

printf("Voici le contenu de ma liste : \n");

/* pour chaque élément e dans la liste l */
liste_foreach (e, l) {
    printf(" * %d\n", liste_get_data_int(e));
}

Élégant, n'est-ce pas ?

Vers de la programmation orientée objet en C

J'ai loin d'avoir montré toutes les subtilités des types opaques en C, mais je pense que ceux qui ont déjà programmé dans des langages (orientés) objet verront dans les fonctions liste_get_suivant et liste_get_data_int des sortes d'« accesseurs » (un « getter », plus exactement) pour les éléments de notre liste chaînée. Dans notre exemple, nous n'avions pas besoin de plus, mais il serait tout à fait aisé d'imaginer les « setters » et autres « méthodes ».

Si on voulait écraser une valeur dans un élément d'une liste, il suffirait d'écrire une fonction dont le prototype est :

void liste_set_data(liste_t element, void* data);

Ce que j'ai montré ici ressemble donc presque à une sorte de façon détournée de faire de la programmation orientée objet en C. Mais en fait, c'est plus ou moins la façon dont c'est implémenté : en C, il est d'usage que lorsqu'on code une fonction qui opère sur une structure, que le premier paramètre de cette fonction soit cette structure. De la même façon que toute méthode d'une classe reçoit systématiquement comme paramètre caché le pointeur this en C++.

J'en profite enfin pour faire un petit peu de publicité pour le blog de Nausicaa et son article « Le pessismisme français : radiographie d'une représentation ». Un post très intéressant que j'ai lu avec beaucoup de plaisir sur un blog qui cherche à démystifier toute un tas d'idées reçues plus ou moins en lien avec l'actualité.

Posté par x0r à 2 commentaires • Tags : programmation c code opaque type types struct malloc