none
NamedPipe bloquant ET non RRS feed

  • Discussion générale

  • Bonjour,

    Je cherche à faire communiquer 2 threads en C#.
    Pour cela j'ai voulu mettre en place un pipe. Un thread qui écrit dedans (bloquant si plein) et un autre qui lit (non bloquant si vide). En général sur les systèmes que je connais c'est soit tout bloquant soit rien bloquant. Donc je m'en sors en choisissant le mode bloquant et en interrogeant "est-ce qu'il y a qqch à lire" avant la fonction de lecture. Mais voila les systèmes que je connais c'est pas Windows et pas C# (je viens de l'embarqué). Donc je me tourne vers vous pour progresser sur ces nouveaux chemins.

    Pour le moment j'ai un code qui ressemble à :

    Pour la création dans un premier thread

    System.IO.Pipes.NamedPipeServerStream pipeServer = new System.IO.Pipes.NamedPipeServerStream("BackToFrontData", 
                                                            PipeDirection.InOut, 
                                                            1, 
                                                            PipeTransmissionMode.Byte, 
                                                            PipeOptions.Asynchronous, 
                                                            100, 
                                                            100);
    ....
    pipeServer.WaitForConnection();
    .....
     
    tmp[0] = (Byte)pipeServer.ReadByte(); // c'est ce read qui ne doit pas être bloquant
    .....

    et dans le second thread :

    System.IO.Pipes.NamedPipeClientStream pipeClient = new System.IO.Pipes.NamedPipeClientStream("BackToFrontData");
    ...
    pipeClient.Write(data, 0, 2);  // c'est ce write qui doit attendre si le pipe est plein
    .....
    .....

    Voila je débute en C#. Avant j'ai un peu développé en C sous windows et je me souviens qu'avec les API WIN32 j'avais des options et des API qui me permettaient de le faire. Mais il faut évoluer de temps en temps donc je suis passé à C# et la je suis bloqué.

    Merci pour votre aide.

    • Type modifié Aurel Bera mercredi 17 octobre 2012 07:22 Pas de reponse
    lundi 8 octobre 2012 07:38

Toutes les réponses

  • Bonjour,

    Aucun avis ?

    Cordialement

    mardi 9 octobre 2012 07:38
  • Bonjour,

    J'ai bien pris le temps de lire les 2 liens mais ça ne fonctionne pas.

    Ce qui me dérange c'est que le même mécanisme fonctionne très bien avec d'autres OS (OS9, VxWorks, QNX, etc....) donc mon besoin est partagé par d'autres developpeurs embarqué et que donc ça doit exister sous windows mais je n'y arrive pas.

    Cordialement

    mardi 9 octobre 2012 12:51
  • Bonjour

    J’ai teste l’exemple dans le premier lien et ça marche très bien pur moi.  

    Avez-vous un message d’erreur?

    Pouvez-vous ajouter un petit exemple pour le client et le serveur ?

    Cordialement,


    Aurel BERA, Microsoft
    Microsoft propose ce service gratuitement, dans le but d'aider les utilisateurs et d'élargir les connaissances générales liées aux produits et technologies Microsoft. Ce contenu est fourni "tel quel" et il n'implique aucune responsabilité de la part de Microsoft.

    mardi 9 octobre 2012 13:24
  • Bonjour,

    Je crée le ServerNamedPipe dans le thread d'interface tel que :

    System.IO.Pipes.NamedPipeServerStream pipeServer = new System.IO.Pipes.NamedPipeServerStream("BackToFrontData",                                                                                                  PipeDirection.In,                                                                                                  1,                                                                                                  PipeTransmissionMode.Byte,                                                                                                  PipeOptions.Asynchronous,                                                                                                  100,                                                                                                  100);


    Puis je crée le ClientNamedPipe dans un autre thread tel que :

    System.IO.Pipes.NamedPipeClientStream pipeClient = new System.IO.Pipes.NamedPipeClientStream(".", "BackToFrontData", PipeDirection.Out, PipeOptions.Asynchronous);pipeClient.Connect();...


    Enfin j'ecris dans le pipe depuis le client comme suit :

    pipeClient.Write(data, 0, 2); // il faut ce write bloquant si pipe plein



    et je lis dans le thread server (interface) sur echeance d'un timer comme suit :

    pipeServer.Read(tmp,0,1); // il faut ce read non bloquant si pipe vide

    Si le read dans le thread interface est bloquant la fenetre reste figée.

    En meme temps le thread qui ecrit dans le pipe a bcps de calcul c'est pourquoi j'ai 2 thread (interface et background_worker).

    Je ne souhaite pas utiliser CheckForIllegalCrossThreadCalls = false pour faire propre.

    Cordialement

    mardi 9 octobre 2012 15:08
  • Bonjour

    Essayez de n’utiliser PipeDirection.InOut si vous n’avez pas vraiment besoin. 

    Utilisez PipeDirection.In et PipeDirection.Out selon le cas.

    Cordialement, 


    Aurel BERA, Microsoft
    Microsoft propose ce service gratuitement, dans le but d'aider les utilisateurs et d'élargir les connaissances générales liées aux produits et technologies Microsoft. Ce contenu est fourni "tel quel" et il n'implique aucune responsabilité de la part de Microsoft.

    mardi 9 octobre 2012 15:14
  • essayé, pas mieux !

    Merci

    mardi 9 octobre 2012 16:51
  • Bonjour,

    Tout d'abord je n'ai pas d'expérience concrète sur le sujet. Je crois comprendre que c'est deux threads dans le même process ? Si oui, est-on vraiment obligé d'utiliser les named pipes (ou c'est parce que potentiellement cela pourrait être sur deux machines différentes ?). Eventuellement une vue d'ensemble de ce que l'on cherche à faire pourrait aiguiller vers une solution plus simple selon le contexte exact (ou éventuellement de faire plus facilement un test répondant au but visé).

    Si on reste avec cette approche voir peut-être du côté de BeginRead voire ReadAsync si .NET 4.5 (je ne suis pas sûr du rôle du timer, c'est une tentative pour faire du "polling") ?


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    mardi 9 octobre 2012 17:13
    Modérateur
  • Bonjour

    J’ai fait plus de tests, et je veux ajouter un marceau de code qui fonctionne pour moi.

    J’ai utilisée  StreamReader et StreamWriter pour Ecrire/Lire. Ca fait les choses plus simples.

    Ce que j’ai remarqué,  vous devez appeler Flush () après chaque écriture.

     Sinon l’info reste dans une mémoire cache. La méthode Flush () existe aussi pour  NamedPipeClientStream, donc pour le debout justement ajouter une ligne pipeClient.Flush () apres pipeClient.Write ().

    Aussi, dans la partie serveur (Interface)  ajoutez  Application.DoEvents();

    Comme ça vous allez voir les updates de la fenêtre de l’application.

    Et le code:

            private void button1_Click(object sender, EventArgs e)
            {
                System.IO.Pipes.NamedPipeServerStream pipeServer = new System.IO.Pipes.NamedPipeServerStream("BackToFrontData", 
                        PipeDirection.In, 
                        1, 
                        PipeTransmissionMode.Byte,
                        PipeOptions.Asynchronous, 100, 100);
                backgroundWorker1.RunWorkerAsync();
                Byte[] tmp = new Byte[4] ;
                tmp[1] = 0;
                pipeServer.WaitForConnection();
                using (StreamReader sw = new StreamReader(pipeServer))
                {
                    int i = 0;
                    while (true)
                    {
                        try
                        {
                            String ln = sw.ReadLine();
     
                            textBox1.Text += ln + "\r\n";
                            Application.DoEvents();
                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show(ex.Message);
                        }
                    }
                }
     
                
            }
            private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
            {
                using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".",
                                "BackToFrontData",
                                PipeDirection.Out,
                                PipeOptions.Asynchronous))
                {
                    try
                    {
                        pipeClient.Connect(2000);
                    }
                    catch
                    {
                        MessageBox.Show("The Pipe server must be started in order to send data to it.");
                        return;
                    }
                    using (StreamWriter sw = new StreamWriter(pipeClient))
                    {
                        int i = 0;
                    while (i< 100)
                    {
                        sw.WriteLine("L_" + i.ToString());
                        sw.Flush();
                        i++;
                        System.Threading.Thread.Sleep(500);
                        
                    }
                    sw.Close();
                    }
                }
            }

    Cordialement,

    Aurel BERA, Microsoft
    Microsoft propose ce service gratuitement, dans le but d'aider les utilisateurs et d'élargir les connaissances générales liées aux produits et technologies Microsoft. Ce contenu est fourni "tel quel" et il n'implique aucune responsabilité de la part de Microsoft.


    • Modifié Aurel Bera mercredi 10 octobre 2012 08:14
    mercredi 10 octobre 2012 08:13
  • Je crois comprendre que l'on voudrait donc :
    - un thread d'arrière plan qui génère des données de temps en temps
    - une lecture non bloquante dans le thread qui gère l'interface utilisateur (donc je dirais qu'un BeginRead devrait le faire)

    Je vais faire un essai. Sinon je suggère de réagir au code posté par Aurel car j'ai l'impression que pour l'instant on teste chacun du code différent ce qui rend difficile de progresser vers une solution...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    mercredi 10 octobre 2012 09:59
    Modérateur
  • Cela me donne par exemple :

            private NamedPipeServerStream server = new NamedPipeServerStream("nom");
    
            private void bw_DoWork(object sender, DoWorkEventArgs e)
            {
                Stopwatch s=new Stopwatch();
                s.Start();
                var client = new NamedPipeClientStream("nom"); 
                client.Connect();
                string str;
                byte[] b;
                while (s.ElapsedMilliseconds <= 30000)
                {
                    str = DateTime.Now.ToString();
                    b=System.Text.Encoding.Default.GetBytes(str);
                    client.Write(b, 0, b.Length);
                    System.Threading.Thread.Sleep(500);
                }
                str = "Close";
                b = System.Text.Encoding.Default.GetBytes(str);
                client.Write(b, 0, b.Length);
                client.Close();
            }
    
            private byte[] b;
            private int c;
    
            private void Form1_Load(object sender, EventArgs e)
            {
                Debug.WriteLine("UI:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
                backgroundWorker1.RunWorkerAsync();
                server.WaitForConnection();
                b = new byte[255];
                IAsyncResult ar = server.BeginRead(b, 0,b.Length, DoRead, null);
            }
            private delegate void UpdateUIDelegate(string str);
            
            private void UpdateUI(string str)
            {
                textBox1.Text = str;
            }
            private void DoRead(IAsyncResult ar)
            {
                c++;
                Debug.WriteLine("Read {0}:{1}", c,System.Threading.Thread.CurrentThread.ManagedThreadId);
                int size=server.EndRead(ar);
                string str = System.Text.Encoding.Default.GetString(b, 0, size);
                textBox1.Invoke(new UpdateUIDelegate(UpdateUI),str);
                if (str != "Close") ar = server.BeginRead(b, 0, b.Length, DoRead, null);
                else
                {
                    server.Close();
                }
            }

    Donc avec un backgroundWorker qui envoie des messages toutes les 500 ms pendant 30 s. J'ai mis aussi qq checkbox sur le formulaire et je vois que l'interface reste réactive, le BeginRead permettant de ne pas bloquer le thread principal.

    Comme je disais , si le but est simplement de communiquer entre le thread d'arrière plan et le thread de l'UI, le contrôle backgroundWorker comporte déjà ce qu'il faut (on peut appeler ReportProgress ce qui déclenche un évènement exécuté sur le thread de l'UI). Donc l'intérêt est de tester spécifiquement la communication via NamedPipes si ce mécanisme est obligatoire dans votre contexte (sinon on peut obtenir exactement le même résultat avec juste un BackgroundWorker).


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".



    mercredi 10 octobre 2012 11:03
    Modérateur
  • Bonjour,

    même process, même machine.

    Cordialement

    mercredi 10 octobre 2012 14:40
  • Entretemps j'ai posté un exemple de code qui utilise BeginRead pour faire un Read non bloquant depuis le thread de l'UI.

    Sinon si c'est le même process et la même machine, les canaux nommés ne me semblent pas forcément avoir le meilleur rapport simplicité/fonctionnalité selon ce que l'on veut faire. Comme je disais, il faudrait éventuellement comprendre le but général. Si par exemple le but est d'avoir une interface utilisateur à partir de laquelle on lance des traitements en arrière plan,  avec éventuellement possibilité pour ce traitement d'être annulé ou de signaler son avancement au thread d'avant plan, je suggère de commencer par le contrôle BackgroundWorker qui est conçu spécifiquement pour cela.


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".


    mercredi 10 octobre 2012 15:49
    Modérateur
  • Bonjour,

    Est-ce que vous avez testé les solutions proposées ? Merci de partager avec nous les résultats, afin que d'autres personnes avec le même problème puissent profiter de cette solution.

    Cordialement,

    Aurel


    Aurel BERA, Microsoft
    Microsoft propose ce service gratuitement, dans le but d'aider les utilisateurs et d'élargir les connaissances générales liées aux produits et technologies Microsoft. Ce contenu est fourni "tel quel" et il n'implique aucune responsabilité de la part de Microsoft.

    jeudi 11 octobre 2012 14:49
  • Bonjour,

    Pouvons-nous considérer que vous avez résolu votre problème avec les scénarios proposés ? Dans l'affirmative, pourriez-vous partager avec nous la solution, afin que d'autres personnes avec le même problème puissent profiter de cette solution ?

    Désormais, nous marquons les solutions proposées. N'hésitez pas à revenir et supprimer la réponse marquée si la solution n’est pas correcte. Merci !

    Cordialement,

    Aurel


    Aurel BERA, Microsoft
    Microsoft propose ce service gratuitement, dans le but d'aider les utilisateurs et d'élargir les connaissances générales liées aux produits et technologies Microsoft. Ce contenu est fourni "tel quel" et il n'implique aucune responsabilité de la part de Microsoft.

    vendredi 12 octobre 2012 07:29
  • Bonjour,

    Désolé de ce mutisme relatif. Je n'est pas pu encore tester. Je teste dans la journée et evidemment je vous tiens informé.

    Merci dans tous les cas de votre implication.

    Cordialement

    vendredi 12 octobre 2012 07:52
  • Bonjour,

    J'ai pas bcps de recul mais il me semble que je peut m'en tirer avec ReportProgress. C'est surement moins bien qu'avec le pipe mais dans un premier temps ....

    Le pipe (mais sous Windows c'est peut-être pas exactement la même définition que celle d'autres OS) présente l'avantage d'un buffer organisé en FIFO, avec en plus des mécanismes de synchro qui me permettent de ne pas perdre de données.

    Un périphérique que je développe communique via USB (vitesse réglable). il émets donc des données totalement irrégulières. Ces données sont lues par le thread de fond et traitées (+ réponse au périphérique si nécessaire) ensuite une structure (au sens C) est envoyée au thread d'interface. Quand je reçois bcps de données il se peut que le thread d'interface n'arrive pas a suivre, mais il faut que le thread de fond pédale à fond. D’où l’intérêt d'un buffer (assez grand) entre les deux threads. Respecter l'ordre des trames est plus important que perdre des trames, d'où l’intérêt de l’écriture bloquante dans un pipe plein. Mais l'interface doit rester fluide, ergonomique et confortable donc il ne faut pas la bloquer, d'où l’intérêt d'un read pas bloquant. ReportProgress j'avais rapidement lu un truc dessus mais j'avais pas saisie qu'il pouvait être utilisé uniquement en "générateur" d’événement. Bien sur je perds la possibilité d'avoir mon application repartie sur 2 PC mais c'est pas grave. Donc j'ai un pipe qui sert de buffer-FIFO bloquant pour le thread de fond et qui émets un événement quand il à fait un write. Le thread d'interface vient lire à ce moment là et il sera pas  bloqué car il sait que l'on vient d'y écrire (mais c'est moyen).

    En revanche je vois des cas de problèmes.  Si mon thread de fond écrit 2 structures dans le pipe avant que le thread d'interface ait la main il ne sera vu qu'un événement ProgressReport donc je viens en dépiler qu'un sur 2 empilés. Dans un cas lent ou je peut de dépiler au rythme où j'empile ça va mais dans le cas plus que probable ou j’écris plus vite que je lis je vais avoir un problème fonctionnel.

    La solution mettant en œuvre le BeginRead à le problème que la callback n'est pas forcement exécutée dans le thread d'interface d'où l'utilisation de .Invoke, ce que me semble revenir à utiliser CheckForIllegalCrossThreadCalls = false

    Enfin le StreamReader je l'ai pas compris et c'est pas un truc que je connais en embarqué donc je vais prendre plus de temps pour le mettre en œuvre. C'est peut-être intéressant mais il me faut prendre le temps d'apprendre.

    Voila, donc merci pour votre aide.

    J'ai pas encore completement solutionné mon probleme mais j'y crois toujours....

    Cordialement

    vendredi 12 octobre 2012 09:41
  • Ok, je vois mieux. Le pipe ne me semble pas nécessaire.

    L'interface utilisateur ne peut être mise à jour que depuis le thread de l'UI. Depuis une certaine version de .NET, on a une vérification automatique que c'est bien le cas (débrayable sans doute pour éventuellement le tester en debug et désactiver si on est sûr de son coup en release à moins que ce soit déjà le cas par défaut ??).

    Invoke (ou BeginInvoke) permet d'appeler une fonction sur le thread de l'UI et ne désactive donc pas cette vérification. Cela nous met juste dans la condition voulue.

    Donc à ma connaissance, aucune autre solution que de mettre à jour l'UI depuis le thread de l'UI. BackgroundWorker permet de générer automatiquement l'évènement de mise à jour sur le thread de l'UI et on a plus à se préoccuper des invokes et cie.

    Après une erreur fréquente est d'appeler trop fréquemment la mise à jour de l'UI ce qui monopolise à nouveau ce thread. Généralement je conseille d'accumuler les infos et d'appeler reportprogress quand on atteint un certain seuil (en volume de données ou en temps) ce qui permet d'éviter de mettre à jour trop souvent l'UI et de la geler à nouveau.

    Donc au final j'essaierai donc un simple BackgroundWorker en accumulant les données et en appelant ReportProgress par exemple toutes les 500 ms et/ou lorsqu'un volume maxi de données est atteint, tout le but étant de ne pas appeler trop souvent la mise à jour de l'UI...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    vendredi 12 octobre 2012 10:14
    Modérateur
  • 500 ms c'est long !

    en 500 ms le thread de fond peut ne rien avoir reçu comme avoir reçu bcps de chose. Et si j'attends 500ms je risque par moment de saturer, il me faut vider le buffer au fil et à mesure.

    De plus attendre un temps m'oblige à lancer un timer et donc de compliquer encore un peu plus la solution finale. Et attendre un certain volume de donnée m'oblige à gérer par ailleurs le taux de remplissage de ce buffer (forcement surdimensionné), le taux de remplissage du buffer (ou le nombre d'octet dedans) c'est exactement ce que je cherche dans un Namedpipe d'où ma question à l'origine de ce thread.

    Je reçois vraiment mes données dans le thread de fond par avalanche.

    Cordialement

    vendredi 12 octobre 2012 13:09
  • C'est juste un rythme minimum de mise à jour de l'interface. Ce n'est ni un timer ni un rythme de lecture. Je ne sais pas quel est le protocole de ce dispositif mais si le buffer est plein à priori le transfert est bloqué pour vous et la lecture va juste vous retourner autant de données que vous avez demandé et vous lirez le reste tout de suite à la prochaine lecture ? En pseudo-code, j'imaginais donc qq chose comme :

    tant que actif
         Taille=LectureBloquanteAvecTimeoutOuNonBloquante
         TempsEcoulé=TempsActuel-TempsDernierReportProgress
         TailleTotale+=Taille // Taille Totale
         si TailleTotale>SeuilTaille ou (TailleTotale>0 et TempsEcoulé>SeuilTemps) alors
                ReportProgress DonnéesReçues
                TailleTotale=0
                TempsDernierReportProgress=0
         fin si
     fin tant que

    A vous de trouvez les seuils qui vont bien dans votre cas.

    Avec une lecturebloquanteavectimeout : on bloque temporairement un peu, la boucle doit continuer pour vérifier si il reste un reliquat récupéré précédemment et éventuellement le signaler si le dernier rafraichissement de l'UI commence à dater.

    Avec une lecturenonbloquante, vous allez juste vérifier le progrès en permanence en arrière plan mais aussi pouvoir recevoir des données même lorsqu'une lecture est en cours (bien pensez à synchroniser correctement les données partagées par la vérif du progrès et la lecture).

    Le point crucial est que l'on n'appelle pas en permanence la mise à jour de l'interface utilisateur ce qui bloquerait à nouveau l'interface.

    Pour le buffer on doit pouvoir utiliser deux buffers pour pouvoir éventuellement faire le progressreport avec un buffer pendant que la callback du BeginRead opère sur l'autre buffer.

    Cela devrait être relativement facile à tester avec deux applications et donc peut-être toujours un named pipe pour tester ce que cela donne sous une charge donnée.


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    vendredi 12 octobre 2012 16:50
    Modérateur
  • Bonjour,

    A priori les timeout ne sont pas disponibles sur un NamedPipe en C#. Du moins j'ai une exception qui me dit cela.

    Il me faudrait donc passer par éventuellement un BeginRead. En oubliant le pipe il n’existe pas sous Windows (et accessible en C#) un mécanisme de communication entre thread qui gère la synchro  en même temps (une sorte de Fifo) ? parce que là je pense que je vais pas m'en sortir.

    Si mon thread de fond empile 2 trames et signale 2 ReportProgress sans que l'UI n'ai eu le temps de prendre la main, l'UI n'en verra qu'une et une trame restera bloquée.

    Je voudrais un système simple et efficace, le pipe sur d'autres systèmes est simple mais peut-être que sous Windows il ne me faut pas chercher un pipe, juste un problème de définition. Mais je nage....

    Cordialement

    lundi 15 octobre 2012 13:02
  • Ok, au temps pour moi. Donc un BeginRead devrait faire l'affaire avec pour la FIFO qq chose comme http://msdn.microsoft.com/en-us/library/dd267265.aspx ?

    Je vais voir si je peux essayer de reprendre mon exemple précédent (actuellement en fin de discussion) éventuellement en deux applis pour ne traiter que le problème qui nous occupe et qui serait donc la réception (et l'autre appli ferait un envoi soit modéré soit en rafale). Comme je disais, je pense qu'il faudrait ensuite voir sur un code précis les points qui ne vont pas sinon je crains que l'on continue de discuter d'une façon trop abstraite pour avancer...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    lundi 15 octobre 2012 17:48
    Modérateur
  • Faudrait en savoir plus sur l'archi de votre appli mais pourquoi n'utilisez vous pas les collections ThreadSafe du framework ? vous pouvez accéder aux données de ces collections entre threads.

    Elles sont dans System.Collections.Concurrent


    Richard Clark
    Consultant - Formateur .NET
    http://www.c2i.fr
    Depuis 1996: le 1er site .NET francophone


    mardi 16 octobre 2012 07:16
  • Bonjour,

    Ça me parait intéressant. Il me faut maintenant l'apprivoiser cette classe.

    Je vais voir ce que je peut en faire.

    En revanche je ne sais pas si je pourrai vous réponse car se loger sur ce site devient de plus en plus difficile.

    Cordialement

    jeudi 25 octobre 2012 12:19
  • Je l'utilise pas parceque je connais pas. Je viens d'autres systemes. Je commence mon apprentissage C#, .net et silverlight.

    Je pars avec 20 ans de developpement C/C++ et ASM mais 0 en C#.

    Comme deja dit precedement se logger sur ce site commence à ressembler au parcour du combattant, aussi je vous remercie de votre aide mais je suis pas certain de pouvoir vous donner l'aboutissement de ce thread, je m'en excuse par avance si c'etait le cas.

    Cordialement

    jeudi 25 octobre 2012 12:23
  • Sinon, j'avais donc fait un essai avec une appli qui envoie et une appli qui reçoit en utilisant simplement un BeginRead et cela semblait apparemment suffisant...

    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    jeudi 25 octobre 2012 16:54
    Modérateur