NSStream et stderr

tabliertablier Membre
août 2013 modifié dans API AppKit #1

Dans Hatari, j'ai des tas de fprintf et fputs ..... etc qui envoient les données sur stderr. Je souhaite rediriger ces données dans une fenêtre type NSWindow.  Je suis en train de regarder du coté de NSStream puisque la définition de fprintf est:


  int fprintf(FILE * restrict stream, const char * restrict format, ...); 


Est-ce que je me plante? Vaut-il mieux regarder autre chose?


Réponses

  • Une solution est de réouvrir stderr vers un fichier (fonction C freopen).


    Puis en parallèle lire ce fichier pour l'afficher dans une NSWindow.


  • AliGatorAliGator Membre, Modérateur
    Regarde peut-être du côté de NSPipe qui permet de faire un "pipe" (pour rediriger la sortie d'un programme vers l'entrée du programme suivant, comme tu ferais "prog1 | prog2" en shell quoi).

    Quitte à  regarder des exemples sur Google, y'en a pas mal sur NSTask (http://borkware.com/quickies/one?topic=nstask, http://www.raywenderlich.com/36537/nstask-tutorial) qui permet de lancer, via du code Cocoa, un programme comme tu le lancerais en ligne de commande. En effet, NSTask est un des cas les plus courant d'usage de NSPipe, puisqu'il faut rediriger la sortie (stdout ou stderr) du programme que tu lances avec un objet NSPipe pour pouvoir lire la sortie en question. Du coup même si toi tu n'utiliseras pas forcément de NSTask (quoique ?), ça te donnera peut-être un peu des pistes pour utiliser NSPipe et rediriger la sortie d'erreur stderr d'un programme vers ton programme Cocoa pour le lire par code et l'afficher dans ta NSWindow.
  • FKDEVFKDEV Membre
    août 2013 modifié #4
    Tu peux toujours :

    #define fprintf myfprintf


    C'est moins elegant que rediriger stderr mais ça marche.
  • Curieux, j'ai ajouté une réponse hier et je ne la retrouve pas ici.


    Je disais que, dans d'autres programmes et sur les conseils reçus des adhérents du forum, j'utilise NSTask, NSPipe,  NSOperation .....etc.


    L'autre développeur avec qui je travaille viens de me dire que la partie multiplateforme (en C) doit être touchée le moins possible. Je vais voir ce qui devient le plus facile dans ce que vous avez conseillé.


  • AliGatorAliGator Membre, Modérateur
    • Si c'est ton programme Cocoa qui va/peut lancer la ligne de commande hatari, tu n'as rien à  faire côté C/hatari (tu n'as pas à  y toucher), tu as juste à  écrire du code Cocoa qui utilise NSTask pour lancer la ligne de commande hatari et NSPipe pour récupérer la sortie de cette NSTask (stdout et/ou stderr) et l'afficher dans une fenêtre (cf tutos à  la pelle sur Google). Et là  du coup c'est très simple, rien à  changer dans le code C de hatari, et que des lignes qu'on trouve un peu partout pour la NSTask+NSPipe côté Cocoa.
    • Par contre si tu comptais avoir hatari lancé en ligne de commande dans un terminal de manière indépendante, et donc que ce ne soit pas ton programme Cocoa qui le lance avec une NSTask mais l'utilisateur qui le lance lui-même, c'est un autre problème :
      • Soit tu peux quand même imposer, quand l'utilisateur ou ton autre process lance hatari, à  ce qu'il redirige stderr vers un fichier ou un pipe ("hatari 2>/var/log/hatari/error.log" par exemple) " à  moins que hatari ecrive d'ailleurs déjà  ses logs dans un fichier en plus de les écrire dans stderr " et du coup tu irais lire ce fichier avec ton code Cocoa pour l'afficher dans ta NSWindow, et problem solved
      • Soit tu ne peux pas imposer ça, tu sais qu'il n'y a vraiment que dans stderr que hatari va sortir ses logs que tu veux récupérer et tu ne peux même pas imposer à  l'utilisateur au lancement de dupliquer stderr dans un fichier ou un pipe comme au point précédent, et dans ce cas tu es un peu bloqué. Il y a bien des solutions avec gdb pour rentrer dans le process de hatari une fois ce dernier lancé pour faire un appel à  creat+dup2 pour dupliquer stderr même après que hatari a été lancé, mais ça fait quand même un peu hack...
    • Bref, si tu pouvais être un peu plus précis à  savoir qui lance hatari (ton programme Cocoa avec ta NSWindow, ou l'utilisateur), si c'est pas par NSTask, est-ce qu'on peut imposer lors du lancement de hatari de dupliquer la stderr avec un "2>/path/to/file", etc...
  • C'est l'utilisateur qui lance Hatari par un clic sur son icône. Pour ouvrir le debugger, il faut cliquer sur un item du menu principal. Attention, ce n'est pas un debugger intel, c'est un débugger "ligne de commande"  680xx  interne à  l'émulation. 


    Si je lance Hatari dans le terminal en appelant l'exécutable dans le dossier MacOs, j'ouvre le debugger par un clic sur l'item de menu. Le debugger marche alors normalement dans le terminal. Voir exemple ci-dessous: le lancement de l'application, l'entrée en mode debug, le désassemblage de 8 instructions et retour à  l'émulation.


    Le debugger est entièrement dans la partie .C et utilise des readline(input), fputs(xx, stderr), fprintf(stderr,format,...) ...etc


    Comme c'est multiplateforme, il faut éviter de trop toucher à  la partie en C.



     


    MacBook-MS:~ msaro$ /Users/msaro/Desktop/Hatari.app/Contents/MacOS/Hatari


    2013-08-29 17:47:58.496 Hatari[11701:903] Could not connect the action fax: to target of class NSView

    Hatari v1.7.0, compiled on:  Aug 29 2013, 13:13:15

    Configured max Hatari resolution = 832x576.

    Support for Hatari window reparenting not built in

    Inserted disk '/Users/msaro/Desktop/Emulateurs/Hatari/disquettes/BOLO.st' to drive A:.

    Building CPU table for configuration: 68010 (compatible mode)

    GEMDOS HDD emulation, C: <-> /Users/msaro/Desktop/Emulateurs/Hatari/disk-IDE/C.

    GEMDOS HDD emulation, D: <-> /Users/msaro/Desktop/Emulateurs/Hatari/disk-IDE/D.

    GEMDOS HDD emulation, E: <-> /Users/msaro/Desktop/Emulateurs/Hatari/disk-IDE/E.

    GEMDOS HDD emulation, F: <-> /Users/msaro/Desktop/Emulateurs/Hatari/disk-IDE/F.

     



    You have entered debug mode. Type c to continue emulation, h for help.

     

    CPU=$e21dc6, VBL=3154, FrameCycles=83702, HBL=373, LineCycles=150, DSP=N/A

    > d

    $e21dc6 : 4e75                                 rts       

    $e21dc8 : 4267                                 clr.w          -(sp)

    $e21dca : 3f3c 004c                         move.w    #$4c,-(sp)

    $e21dce : 4e41                                  trap           #1

    $e21dd0 : 4eb9 00e2 1e42             jsr              $e21e42

    $e21dd6 : 4e68                                 move         usp,a0

    $e21dd8 : 48e0 7ffe                         movem.l   d1-d7/a0-a6,-(a0)

    $e21ddc : 4e60                                 move         a0,usp

    > c

    Returning to emulation...

     


    Donc, le problème est d'avoir une fenêtre pour le debugger, sans passer par le terminal.


    L'étape suivante serait (conditionnel) d'avoir un debugger un peu plus graphique.


    Ce genre de code me donne un petit coup de vieux!! J'ai travaillé des années avec des 68xxx!!!

  • AliGatorAliGator Membre, Modérateur
    août 2013 modifié #8
    Je ne comprend toujours pas trop dans laquelle des 2 situations tu veux pouvoir te placer pour le cas où tu veux ton débuggeur :
    • C'est pour toi uniquement, tu veux remplacer la solution " je tape "/path/to/Hatari.app/Contents/MacOS/Hatari" dans le terminal + voit la sortie console " par une solution " je lance mon appli maison HatariConsole.app faite par mes soins qui se charge de lancer Hatari.app et redirige son stderr pour afficher la console dans une NSWindow " ? (ça, c'est vraiment pas dur à  faire)
    • Tu veux pouvoir laisser l'utilisateur lancer "/path/to/Hatari.app/Contents/MacOS/Hatari" dans leur terminal d'un côté, et ensuite lancer manuellement après coup ton appli maison HatariConsole.app" ?
    • Tu veux à  terme pouvoir laisser l'utilisateur lancer Hatari.app par un double-clic et une fois que l'application Hatari.app est lancée, lancer ton HatariConsole.app qui va capturer les logs de Hatari.app et les afficher dans une NSWindow ?
    Dans tous les cas, pour capturer stderr, il faut rediriger la sortie standard stderr de Hatari.app vers un fichier ou un pipe UNIX pour pouvoir le capturer, et ça ça se fait quand tu exécutes la commande pour lancer Hatari.app, et non pas après coup (une fois l'appli Hatari.app lancée, c'est trop tard pour rediriger sa sortie autre part)
    • Donc pour la première solution, pas de soucis, si c'est ton appli HatariConsole.app qui se charge de lancer Hatari.app bah elle pourra au moment où elle le lance rediriger les sorties standard via un NSPipe vers un NSStream et le lire
    • Mais pour les 2 dernières solutions, comme ce n'est pas ton appli qui lance Hatari.app, il faut absolument rediriger la sortie standard qqpart pour que ton appli HatariConsole.app puisse la relire. Donc soit tu demandes à  l'utilisateur quand il lance Hatari.app de rediriger vers un fichier donné, par exemple "/path/to/Hatari.app/Contents/MacOS/Hatari 2>/var/log/hatari/hatari.log", soit tu modifies le code de Hatari (puisque tu as l'air de dire que tu t'autorise à  taper un minimum dans le ".C" du moment que ça reste portable) pour qu'il écrive dans autre chose que stderr (par exemple /var/log/hatari/hatari.log)
    Parce que même si tu trouvais un moyen de lire directement dans /dev/stderr avec un NSStream (ce qui en pure théorie doit être faisable), non seulement tu te retrouverais avec TOUT ce qui tombe dans /dev/stderr (donc les logs d'erreur de tous les programmes qui tournent sur ta machine et bennent des logs dans /dev/stderr, tout mélangé, et pas que ceux de Hatari) mais en plus je ne suis pas sûr que ça marcherait, car il faut dupliquer (fonctions C creat+dup2) la sortie standard pour pouvoir lire dedans (sinon si plusieurs programmes essayent de lire le pipe /dev/stderr " qui n'est pas un fichier seekable mais plutôt un pipe/stream au sens UNIX " et donc essayent de consommer en même temps la sortie du buffer, il risque d'y avoir des soucis).
    Or dupliquer une sortie standard d'un programme donné (comme Hatari) pour la rediriger vers un pipe ou fichier autre que le /dev/stderr utilisée par défaut, c'est précisément ce qui est fait quand on fait des NSTask + NSPipe en Cocoa, ou quand on fait "2>/var/log/hatari.log" dans un shell.

    Mais si ton utilisateur lance Hatari.app par un double-clic et que tu ne modifies pas le code de Hatari.C pour qu'il écrive dans autre chose que stderr (à  la place de ou en plus de, au choix), bah tu vas être bloqué. (Au mieux si tu réussis à  mettre au point du code pour lire dans /dev/stderr, tu vas te récupérer tous les messages écrits dans /dev/stderr toutes tâches/programmes confondu[e]s et pas que ceux de Hatari, alors bon...)


    En tout cas, j'ai l'impression que tu n'as toujours pas répondu à  la question qui reste en suspens depuis plusieurs postes : est-ce que c'est ton appli HatariConsole.app qui va lancer l'exécutable Hatari ou pas ?
  • tabliertablier Membre
    août 2013 modifié #9

    C'est l'application qui lance la console debugger et non pas la console debugger qui lance l'application. Mais c'est un peu compliqué car l'application (la partie Mac) se lance seul,  puis lance la partie multiplateforme qui s'initialise et  initialise SDL. C'est mal dit, mais c'est ça!  Le debugger existant est contenu dans la partie multiplateforme.


    Comme le logiciel est compilé séparément pour chaque plateforme, Il doit y avoir moyen avec quelque chose dans un .pch de modifier les  destinations sans toucher au .C.  


     


    Pour les curieux, l'adresse de téléchargement pour un essai:  http://cocoa.pod.free.fr/Cacao/Hatari.zip


    (logiciel + tos image + disquette image)


  • AliGatorAliGator Membre, Modérateur
    Merci de préciser quand tu parles de "l'application" de laquelle tu parles (Hatari.app ou ton application que tu veux faire avec ta NSWindow) car j'avoue que si pour toi c'est clair, à  chaque message j'ai toujours du mal à  voir qui parle à  qui, qui lance qui, qui reçoit les logs de qui... c'est des coups à  s'embrouiller tout ça :D

    Du coup je pense qu'il faut recommencer depuis le début.

    Donc déjà , je viens de réaliser, enfin si cette fois j'ai bien compris, que contrairement à  ce que je pensais et le postolat sur lequel j'étais parti depuis le début :
    1) Tu es en train de travailler sur Hatari.app directement, alors que moi je pensais tu utilisais Hatari.app, sans rien y toucher, comme tu utiliserais Mail.app ou TextEdit.app ou autre, et que tu voulais créer une application à  toi séparée, disons HatariConsole.app, qui aurait pour but de détourner la sortie standard du Hatari.app (une fois lancé) vers une NSWindow
    2) Hatari.app n'est pas juste codé en pur multiplateforme avec que du code indépendant de la plateforme, tu as une partie commune indépendante de la PF et ensuite pour chaque PF (dans ton cas OSX) tu as un wrapper qui contient un peu de code spécifique à  la PF et appelle le code commun ensuite. Ce n'est donc pas ton application hypothétique HatariConsole.app dédiée qui lancerait l'appli existante Hatari.app, tout est dans le même projet

    Cela change carrément toute la donne. Le problème n'est plus du tout le même, ça change l'approche à  prendre ! Du coup exit les NSPipe & co, c'est pas de la communication inter-process et des fichiers ou des pipes ou des devices (/dev/...) pour communiquer entre eux, tu peux directement passer un FILE* du module plateform-specific au module Hatari.C indépendant de la PF et le tour est joué. Ou utiliser un simple appel creat() puis à  dup2 pour dupliquer le FILE* stderr dans ton code spécifique OSX et travailler dessus ensuite comme tu le ferais avec n'importe quel code C !

    J'attend ta confirmation de savoir si c'est bien ça (merci d'être précis dans tes explications pour éclairer ma lanterne pour éviter les confusions cette fois ^^), car si oui ça change tout (si j'avais su / compris dès le début... ^^)
  • Je travaille sur Hatari directement. Les sources comprennent une partie "émulateur" commune, et trois parties spécifiques dans les dossiers gui-osx, gui-sdl, gui-win. Je suppose que gui-sdl est pour linux. L'émulateur est dans la partie commune.


    La fenêtre du debugger doit être intégrée dans Hatari et tout sera dans le même projet. Du moins, c'est ce que souhaite le développeur avec qui je travaille. Je ne cherche pas à  écrire un debugger, juste a ajouter une entrée de commande et une visualisation faciles au debugger intégré, en modifiant le moins possible la partie commune.  Il me semble que sur un Mac, ça s'impose de mettre en fenêtre le debugger. Le Mac est réputé pour ses GUI!


     


      >:(   Là , on peut se rendre compte de la difficulté de communication quand on le fait par écrit aller-retour. Sachant ce que je cherche, j'ai pensé m'être expliqué clairement!!  ah, ah!! ce n'était pas le cas.


  • AliGatorAliGator Membre, Modérateur
    août 2013 modifié #12
    Ok c'est bien plus clair !
     

    >:(   Là , on peut se rendre compte de la difficulté de communication quand on le fait par écrit aller-retour. Sachant ce que je cherche, j'ai pensé m'être expliqué clairement!!  ah, ah!! ce n'était pas le cas.

    Ouais, enfin je suis aussi complètement décalé sur mon sommeil en ce moment donc ça doit pas aider de mon côté, j'étais persuadé d'avoir compris un truc dès le départ et j'ai mis du temps à  comprendre que j'avais compris de travers du coup j'ai persisté dans mon idée... Y'a des fois je me dis ça serait bien un petit tableau blanc sur CocoaCafe pour faire des schemas vite fait pour expliquer nos questions :P


    Bon pour en revenir à  ton problème, du coup : puisque tu es dans le même process que hatari.C, lire directement ce qui est écrit dans stderr (du coup le stderr du process, donc) devrait marcher je pense. En utilisant +[NSFileHandle fileHandleWithStandardError] tu devrais t'en sortir. Si je ne dis pas de bêtises, on ne peux pas lire directement dans stdout ou stderr, puisque ce sont des flux de sortie (dans lesquels tu écris mais ne lis pas), mais par contre tu peux créer un pipe (NSPipe en Cocoa ou pipe() en C pur), y connecter stderr sur l'entrée, et lire ce qu'il y a à  la sortie du tuyau.

    Ca donnerait un truc du genre (non testé, pondu en live en grapillant des extrait à  gauche à  droite sur le net) :
    // Appeler cette méthode pour te connecter au stderr de ton process et y connecter une méthode quand il y a du contenu qui lui est envoyé
    - (void)captureStandardError
    {
    NSPipe* pipe = [NSPipe pipe]; // Un tuyau (pipe) où on va écrire d'un côté et lire de l'autre
    NSFileHandle* writeHandle = [pipe fileHandleForWriting]; // le côté "écriture" à  l'entrée du pipe
    NSFileHandle* readHandle = [pipe fileHandleForReading]; // le côté "lecture" à  la sortie du pipe
    // dupliquer stderr dans l'entrée du pipe pour envoyer le contenu de stderr dans le tuyau
    dup2([writeHandle fileDescriptor], fileno(stderr));

    // S'abonner aux notifications quand on aura des données à  lire de l'autre côté du pipe
    [[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(dataReceivedFromPipe:)
    name:NSFileHandleReadCompletionNotification
    object:readHandle];
    // Lancer la lecture de la sortie du pipe en arrière-plan (= dès que des données sont dispos, il va les lire et nous notifier)
    [pipeReadHandle readInBackgroundAndNotify];
    }

    // Méthode appellée dès que des données sont arrivées dans le pipe en entrée, ont donc été disponibles en lecture en sortie, et ont été lues à  cause du readInBackgroundAndNotify.
    - (void)dataReceivedFromPipe:(NSNotification*)notif
    {
    // Récupérer les données lues
    NSData* readData = notification.userInfo[NSFileHandleNotificationDataItem];
    // Les transformer en NSString
    NSString *str = [[NSString alloc] initWithData:readData encoding:NSASCIIStringEncoding];
    // Les afficher dans le NSTextView de ta NSWindow
    self.textView.text = [self.textView.text stringByAppendingString:str];

    // Relancer une lecture en tâche de fond pour le futur contenu qui ne manquera pas de continuer d'arriver
    NSFileHandle* readHandle = notif.object;
    [readHandle readInBackgroundAndNotify];
    }
  • Là , tu me donnes une solution complète! c'est sympa!


    Je vais vérifier et tester ça.


     


    Notre problème de communication me rappelle cette histoire:


    2 spécialistes  travaillent ensemble sur un problème ardu qui touche leur deux spécialités. ils sont chacun peu versé dans la spécialité de l'autre. Au bout d'une dizaine d'année de travail, il y en a un qui donne une information à  l'autre. Le deuxième dit "eh bien ça nous permet de résoudre notre problème, pourquoi ne me l'avez vous pas dit il y a dix ans?". La réponse du premier est " ben, ce que je vous dit là  est évident pour moi, et vous ne m'avez jamais posé de question la dessus"

  • tabliertablier Membre
    septembre 2013 modifié #14

    ça marche, je l'ai re-écrit pour compiler sous 10.5 ou 10.6. ça donne ça:



    // Appeler cette méthode pour se connecter au stderr du process et  y connecter une méthode quand il y a du contenu qui lui est envoyé


    //


    - (void)captureStandardError


    {


    pipe = [NSPipe pipe] ; // Un tuyau (pipe) où on va écrire d'un côté et lire de l'autre


    writeHandle = [pipe fileHandleForWriting] ; // côté "écriture" à  l'entrée du pipe


    readHandle = [pipe fileHandleForReading] ; // côté "lecture" à  la sortie du pipe


    // dupliquer stderr dans l'entrée du pipe pour 


    int truc = dup2([writeHandle fileDescriptor], fileno(stderr)); //       envoyer le contenu de stderr dans le tuyau


     


    // S'abonner aux notifications quand on aura des données 


    [[NSNotificationCenter defaultCenter] addObserver:self // à  lire de l'autre côté du pipe


                                               selector:@selector(dataReceivedFromPipe :)


                                                   name:NSFileHandleReadCompletionNotification


                                                 object:readHandle];


    // Lancer la lecture de la sortie du pipe en arrière-plan 


    [readHandle readInBackgroundAndNotify] ; // (= dès que des données sont dispos, il va les lire et nous notifier)


    }


     


    // Méthode appellée dès que des données sont arrivées dans le pipe en entrée, donc disponibles  


    //  en lecture en sortie, et lues à  cause du readInBackgroundAndNotify.


    //


    - (void)dataReceivedFromPipe:(NSNotification*)notif


    {


    NSData* readData = [[notif userInfo] objectForKey:NSFileHandleNotificationDataItem] ; // Récupérer les données lues


    NSString *datum = [[NSString alloc] initWithData:readData encoding:NSASCIIStringEncoding] ; // Les transformer en NSString et les afficher


    [[dbgTextVu textStorage] appendAttributedString:[[[NSAttributedString alloc


    initWithString:datum attributes:attrb] autorelease]] ;


    [dbgTextVu scrollRangeToVisible:NSMakeRange([[dbgTextVu string] length], 0)] ;


     


    [datum release] ;


    [readHandle readInBackgroundAndNotify]; //  attente de lecture


    }



    avec :


    IBOutlet NSScrollView *dbgScroll ;


    IBOutlet NSTextView *dbgTextVu ;


     

     


    J'ai un autre problème avec ça et je réfléchis à  comment formuler la question.


  • AliGatorAliGator Membre, Modérateur
    Cool :)


    Par contre faut faire le scrollRangeToVisible QUE si avant d'ajouter ton text à  la TextView, tu étais déjà  en bas (à  l'offset maximum du scrolling).


    Car sinon c'est chiant pour l'utilisateur s'il scroll vers le haut pour remonter dans les logs et que sans prévenir tu le rescrolles à  la fin alors qu'il était en train de lire ;)

  •  


    Par contre faut faire le scrollRangeToVisible QUE si avant d'ajouter ton text à  la TextView, tu étais déjà  en bas (à  l'offset maximum du scrolling).



    tu as raison, il faut que je regarde ça.


     


    Sans rigoler, j'aimerais bien que la mise en page du code soit conservée lorsque je fais un copier-coller dans l'éditeur des posts du site. Y a t-il une balise spéciale à  utiliser pour ça? peut être "pre"?


Connectez-vous ou Inscrivez-vous pour répondre.