vendredi 11 septembre 2009

Redirections à travers Ocaml

Mes remerciements les plus vifs à un japonais inconnu dont le post(?) sur un blog(?) m'a débloqué! J'essayais de gérer les redirections entre des programmes démarrés à travers Ocaml, et j'avais un mal fou à rediriger les flux. Je n'ai rien compris à son post, juste vu le code, et j'ai compris que je m'y prenais comme un pied.

Maintenant, j'ai une fonction toute bête pour prendre un JPEG en entrée et ressortir le texte en sortie:


let gocr file =
let i_file = Unix.openfile file [Unix.O_RDONLY] 0 in
let gocr_in, jpg_out = Unix.pipe() in
let result, gocr_out = Unix.pipe() in
let pid0 =
Unix.create_process
"jpegtopnm"
[|"jpegtopnm"|] i_file jpg_out Unix.stderr in
Unix.close jpg_out;
let pid1 =
Unix.create_process
"gocr"
[|"gocr"; "-"|] gocr_in gocr_out Unix.stderr in
Unix.close gocr_out;
ignore(Unix.waitpid [] pid0);
ignore(Unix.waitpid [] pid1);
let buffer = Buffer.create 4096 in
let read_buffer = String.create 512 in
let r = ref true in
while !r do
let len = Unix.read result read_buffer 0 512 in
if len > 0 then
Buffer.add_substring buffer read_buffer 0 len
else
r := false
done;
Buffer.contents buffer


L'on ouvre donc le fichier d'entrée, un pipe entre jpegtopnm et gocr, et un pipe entre gocr et notre sortie (si quelqu'un a une idée pour se débarrasser du deuxième pipe, je suis preneur!). L'on chaîne les processus avec les descripteurs de fichiers ainsi récupérés, et, très important, on ferme le pipe d'output après l'avoir donné à un processus. Pour la fin, je lis bêtement depuis mon pipe jusque dans un buffer, jusqu'à ce que le pipe soit vide, et je renvoie le contenu du buffer.

Ceci m'approche d'autant plus du moment où je vais enfin pouvoir faire la reconnaissance de caractères directement depuis le serveur web.

mardi 1 septembre 2009

Ocsigen - Images dynamiques

Voilà un sujet sur lequel il m'a fallu batailler ferme! C'est tout l'intérêt d'Ocaml: la difficulté est d'arriver à écrire du code qui compile. Une fois que le compilo est content, le code fonctionnera comme convenu. C'est exactement ce qui s'est passé avec mes images dynamiques.

Les documents et miniatures sont tous sauvés dans la base de données de Médoc. Quand bien même balancer ce genre de gros blobs à travers du SQL n'est pas franchement recommandé, je pense qu'il s'agissait là d'un des cas de figure autorisés. En effet, sauver ces images dans la base permet de centraliser toutes les données, ce qui simplifie la sauvegarde et la restauration et permet de laisser à la base le soin de chiffrer les données.

Cela veut dire qu'il faut pouvoir générer une image à partir d'un buffer binaire récupéré de la base. Ça tombe bien, Ocsigen sait envoyer du binaire via un Streamlist. Écrivons par exemple un service Ocsigen qui affiche une image à partir d'une chaîne binaire:


(* Petites déclarations ennuyeuses *)
open Lwt
open XHTML.M
open Eliom_services
open Eliom_parameters
open Eliom_sessions
open Eliom_predefmod.Xhtml

(* Déclaration du service, appelé "view", et qui prend *)
(* l'identifiant entier de l'image en paramètre *)
let view = Eliom_services.new_service ["view"] (int "id") ()

(* Initialisation du service *)
let _ = Eliom_predefmod.Streamlist.register view
(fun sp id () ->
return
([fun () ->
return (Ocsigen_stream.of_string (get_data id))],
"image/png"))

Il suffira d'écrire la fonction get_data qui prend dans notre cas l'identifiant entier de l'image à afficher, et qui retournera la chaîne contenant les données binaires de l'image.

Ce service peut ensuite être utilisé dans la déclaration d'un élément img:
 <img src="view?id=2"/>
pour afficher des images à l'intérieur d'une page (attention aux performances quand même, quand on va taper dans la base pour chaque image!).

lundi 31 août 2009

Premiers pas avec Ocsigen

Je voulais m'y mettre depuis longtemps, à ce serveur web. Or, justement, j'avais un petit projet en tête pour me délasser d'AdH: un système de documentation.


Ocsigen est donc un serveur web écrit en Ocaml, qui charge des modules basés sur le framework Eliom. Ocsigen est performant, mais surtout force un typage extrêmement fort du HTML en lui-même, et des liens entre les différents services. Enfin, l'on pourra réutiliser tout le code Ocaml existant, à condition de le faire rentrer dans le modèle de thread, basé sur une bibliothèque de threads coopératifs, Lwt.

Le gros problème d'Ocsigen, c'est qu'en dehors de la documentation, point de salut, ou si peu.

La documentation en elle-même est complète, mais manque furieusement d'exemples si l'on sort un tant soit peu des sentiers battus, et il faut pas mal d'efforts pour commencer à comprendre la manière dont les différents éléments s'enchaînent. L'inférence de types de nos amis Hindley et Milner vient heureusement à notre secours, et il serait une très mauvaise idée d'essayer d'écrire les interfaces avec des types aussi biscornus.

La communauté, elle, est inexistante (ou du moins, très bien cachée), et les sites publics qui tournent sous Ocsigen sont rares. Heureusement, un Wiki a été écrit pour Ocsigen pour le Google Summer of Code, ce qui aide beaucoup à voir comment, par exemple, utiliser Postgresql à travers Lwt.

Il est particulièrement intéressant d'écrire des pages à travers Ocsigen, c'est une manière d'appréhender la programmation Web tout à fait différente. On finit par se faire aux types ésotériques, et j'ai rapidement écrit quelques fonctions pour faciliter l'utilisation d'éléments récurrents.

Mon système de documentation, répondant au doux nom de Médoc (Mes docs :) ), avance tout gentiment, en quelques jours j'ai déjà une base de données (avec chiffrage des données les plus sensibles!), un outil en ligne de commande pour y sauver les documents, et une page web qui permet d'effectuer des recherches et d'afficher les documents. En voici une petite capture:



La page affiche tous les documents correspondant à un terme, en se basant sur le "Full Text Search" de Postgresql. L'on affiche également les miniatures de chaque page, cliquables et qui mènent au document grandeur nature.

Un peu de CSS sera nécessaire pour rendre l'ensemble plus joli, mais le système est déjà utile, ce qui est un bon début!

vendredi 28 août 2009

Postgresql Full Text Search - Planquez ces chiffres!

Admettons que l'on veuille sauver des textes cryptés dans la colonne d'une table Postgresql. Rien de plus facile en utilisant les fonctions pgp_sym_encrypt et pgp_sym_decrypt. Jusque là, tout va bien.

Mais admettons maintenant que l'on veuille également pouvoir faire des recherches rapides sur ces textes en utilisant le Full Text Search. L'on rajoute une colonne de type tsvector, et l'on parse le texte là dedans. Patatras, votre vilain espion peut maintenant recréer le document en regardant ce champ, qui, lui, ne peut pas être crypté!

L'on peut bien sûr monter ses données sur une partition cryptée. Mais c'est compliqué, et pas facile à installer chez l'utilisateur, sans compter que tout est en clair tant que la machine est allumée.

Cependant, admettons encore (ça fait beaucoup, je sais!) que les données sensibles soient plutôt des nombres (codes secrets, identification, numéros de compte, sommes d'argent). Sans ces nombres, les documents deviennent plutôt inoffensifs. Et de plus, il est peu probable que l'on aie besoin de ces nombres pour faire des recherches. C'est parfois utile, certes ("quels sont les documents qui contiennent mon numéro de compte?"), mais on pourra s'en passer.

Ça tombe bien, il est possible de créer une nouvelle configuration qui escamotera ce genre de données. Il suffit d'enlever la correspondance entre les types de tokens et le parseur.

Ajoutons donc la configuration french_safe:

create text search configuration french_safe ( copy = pg_catalog.french );
alter text search configuration french_safe drop mapping for int;
alter text search configuration french_safe drop mapping for uint;
alter text search configuration french_safe drop mapping for float;
alter text search configuration french_safe drop mapping for numword;

Alors que de la configuration de base, tout un tas de données sensibles auraient pu être extraites:


select * from to_tsvector(
'french',
'mon code secret est le 12345 et j''ai 217.45 € sur mon compte')
=> "'12345':6 '217.45':10 'cod':2 'compt':13 'secret':3"


la nouvelle configuration, elle, garde nos petits secrets bien au chaud!

select * from to_tsvector(
'french_safe',
'mon code secret est le 12345 et j''ai 217.45 € sur mon compte')
=> "'cod':2 'compt':11 'secret':3"


Faites donc un peu tourner des phrases sous ts_debug, et voyez comment les nombres et les mélanges chiffres lettres (mots de passes!) sont gentiment effacés.

lundi 17 août 2009

Trading

Petit aperçu de ce que va donner la GUI de trading



L'idée est de permettre au joueur de choisir lui-même la disposition des éléments d'interface, à travers l'utilisation du wxAuiNotebook de chez wxWidgets. Pour l'instant, il n'existe que deux types de composants: la liste des marchandises, et l'inventaire. Je vais y ajouter le carnet d'ordres, et enfin la visualisation des prix du marché en temps réel.

Le serveur est séparé du serveur de jeu AdH, ce qui me permet d'être plus léger, et d'identifier les composants partagés pour les mettre dans une bibliothèque à part. A terme, je devrais avoir migré de nombreux composants, rendus génériques, vers cette bibliothèque, ce qui devrait permettre de créer très facilement des serveurs spécifiques, voire de monter des clusters. Mais pour le moment, j'essaie de trouver un gameplay sympa pour la partie trading. Je compte bien que ça soit le souk, dans le sens premier du terme!

lundi 10 août 2009

Postgresql 8.4 et les requêtes récursives

La fonctionnalité de Postgres 8.4 que je préfère, c'est les requêtes récursives. Voilà vraiment quelque chose qui va permettre de résoudre bon nombre de problèmes de manière beaucoup plus élégante, et notamment tout ce qui concerne les arbres, les graphes, et les hiérarchies de toutes sortes.

Voici par exemple un arbre où chaque élément pointe vers son parent.


create table h(entry int not null, parent int null);

insert into h values(1, null);
insert into h values(2, 1);
insert into h values(3, 1);
insert into h values(4, 1);
insert into h values(5, 1);
insert into h values(6, 2);
insert into h values(7, 2);
insert into h values(8, 6);
insert into h values(9, 8);
insert into h values(10, 4);


Maintenant, répondons à la question: Quels sont tous les éléments enfants de l'élément 2? Avec le SQL récursif, c'est trivial:


with recursive deep(n) as(
select entry from h where entry = 2
union
select entry from h join deep on h.parent = deep.n
)
select * from deep;


Le premier élément de l'union est l'amorce de la récursion: l'on démarre donc avec l'élément 2. Le second élément est la récursion en elle-même: l'on retourne l'ensemble des éléments qui sont parents de l'élément n.

La documentation contient quelques exemples beaucoup plus compliqués, où l'on explore des graphes cycliques, avec un SQL qui devient franchement ésotérique. Mais pour des cas simples, c'est un très bon moyen de se débarrasser des couches de pl/pgsql ou autres, et de revenir en pur SQL, ce qui pourrait entre autres améliorer les performances en permettant au planificateur de travailler directement sur toutes les tables.

A propos de planificateur, le nouveau pgadmin3 (malheureusement pas encore dans Squeeze, mais cela ne saurait tarder) montre de bien jolies choses sur ces requêtes récursives. Voici donc l'explain de ma requête.



En faisant quelques tests supplémentaires, je me suis rendu compte que Postgres utilisait gentiment les index qu'on lui donnait sur les colonnes "entry" et "parent", ce qui permettait de gagner environ 25% de temps sur de très grosses requêtes (arbre de 100 000 éléments). Elle est pas belle, la vie?

jeudi 6 août 2009

Postgresql 8.4 débarque dans Debian Squeeze

C'est en faisant mon "aptitude update; aptitude dist-upgrade" hebdomadaire que j'ai vu avec joie que Postgresql 8.4 allait arriver. Vu qu'apparemment, la 8.3 allait jarter du même coup, je suis allé faire mon petit backup, et j'ai lancé la mise à jour.

Tout c'est bien passé, sauf pour Mediawiki qu'il a fallu un poil bidouiller. En effet, en recréant l'utilisateur wikiuser (souvenez-vous du mot de passe!), l'on perd le "search path" vers le schéma "mediawiki", et l'application se met à se plaindre qu'elle ne trouve pas ses tables. Heureusement, la résolution est aisée:


alter user wikiuser set search_path = mediawiki,public;


Et voilà!