ubuntuusers.de

Wie externes Programm aufrufen? (C/C++)

Status: Gelöst | Ubuntu-Version: Ubuntu 12.04 (Precise Pangolin)
Antworten |

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6518

Wohnort: Hamburg

Ich habe den Titel absichtlich etwas kurz gehalten, da es sonst ein ganzer Absatz geworden wäre.

Ich möchte von einem C++ Programm aus ein anderes Programm starten. Bisher hatte ich das immer mit popen() gemacht. Damit kann man aber immer nur in eine Richtung Daten austauschen. Jetzt benötige ich aber einen Datenaustausch in beide Richtungen. Alles was ich bisher zu diesem Thema gelesen hatte, weist auf Fifos hin und klingt sehr kompliziert. Brauchbare Beispiele für bidirektionale Kommunikation habe ich da aber auch noch nicht gefunden.

Bevor ich da jetzt tiefer einsteige, wollte ich daher die Experten hier mal Fragen, ob ich überhaupt auf dem richtigen Weg bin oder ob es einfachere Alternativen gibt.

Das Scenario: Ich habe ein GUI-Programm das Metadaten (Kommentare) bearbeiten soll. Diese werden oft per Copy/Paste eingefügt, wobei aber nicht alles interessant ist oder neu formatiert bzw. gefiltert werden muss. Die Anforderungen sind dabei je nach Benutzer und Anwendungsfall unterschiedlich, weshalb ich diesen Textfilter nicht fest in das Programm einbauen will. Stattdessen soll der Benutzer die Möglichkeit bekommen, ein externes Programm dafür anzugeben. Das könnte notfalls ein Bash Script oder sed sein. D.h. die Kommunikation soll über stdin/stdout stattfinden.

Geht das so überhaupt?

MPW

Anmeldungsdatum:
4. Januar 2009

Beiträge: 3731

Hallo,

in Anbetracht der Möglichkeit, dass der Nutzer den Server- und Client-Teil auf verschiedenen Rechnern nutzen möchte, solltest du das wohl direkt über Sockets implementieren.

Das ist auch der gängige Weg, wenn du beide Teile das Programms schreibst. Aufrufen von Drittprogrammen und auslesen der Rückgabewerte ist eine Funktion, die zum Einbinden von binären Programmen von dritten gedacht ist. Das trifft in deinem Fall nicht zu, soweit ich das verstanden habe.

Grüße MPW

Lysander

Avatar von Lysander

Anmeldungsdatum:
30. Juli 2008

Beiträge: 2669

Wohnort: Hamburg

Dakuan schrieb:

Das Scenario: Ich habe ein GUI-Programm das Metadaten (Kommentare) bearbeiten soll. Diese werden oft per Copy/Paste eingefügt, wobei aber nicht alles interessant ist oder neu formatiert bzw. gefiltert werden muss. Die Anforderungen sind dabei je nach Benutzer und Anwendungsfall unterschiedlich, weshalb ich diesen Textfilter nicht fest in das Programm einbauen will. Stattdessen soll der Benutzer die Möglichkeit bekommen, ein externes Programm dafür anzugeben.

Nur weil die Anforderungen unterschiedlich sind, muss der Benutzer doch nicht unbedingt ein externes Programm dafür aufrufen! Du kannst doch verschiedenen Strategien implementieren und ihm eine Auswahl ermöglichen?

Oder willst Du im Grunde eine Art Plugin-Schnittstelle, bei der der Benutzer selber komplett frei ist in der Wahl des Werkzeugs?

Das wird dann natürlich immer ein wenig schwieriger...

Wenn es reicht, dass das unter Linux läuft, wäre dbus mein erster Ratschlag. Ansonsten gäbe es mit Boost.Interprocess eine spezielle C++-Lösung.

Imho angenehmer - dafür mit einem Mehraufwand verbunden - wäre eine Message Queue a la RabbitMQ

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6518

Wohnort: Hamburg

Du kannst doch verschiedenen Strategien implementieren und ihm eine Auswahl ermöglichen?

Das mit den verschiedenen Strategien ist so eine Sache. Ich habe da noch keine wirklich gute Idee, mit der man alles abdecken könnte. Im wesentlichen geht es ja auch darum, das bei einer Änderung der zugelieferten Daten nicht jedesmal das eigentliche Programm geändert werden muss.

Aber Du bringst mich da gerade auf eine neue Idee. Da es sich bei dem Filter im wesentlichen um Texttabellen handelt, könnte man diese auslagern, sodass der Benutzer sich für jeden Anwendungsfall eine spezielle Tabelle erstellen / anpassen kann.

Oder willst Du im Grunde eine Art Plugin-Schnittstelle, bei der der Benutzer selber komplett frei ist in der Wahl des Werkzeugs?

Das trifft meine ursprüngliche Idee eigentlich am besten.

Ich werde jetzt erstmal eine Runde mit dem Rad duchs Grüne machen, damit mein Kopf wieder klar wird.

microft

Avatar von microft

Anmeldungsdatum:
6. August 2009

Beiträge: 454

Wohnort: Norddeutschland

Hallo

Schreib deine Eingansdaten einfach in eine temporäre Datei und übergebe diesen File dann, als Parameter, an ein Programm das du mit popen aufrufst und seinen Output einließt. Auf diese weise kann dein Filterprogramm auch ein Script oder ein awk oder sonstwas sein ohne das du für den Filter das Rad neu erfinden mußt.

cu

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13219

microft schrieb:

Schreib deine Eingansdaten einfach in eine temporäre Datei und übergebe diesen File dann, als Parameter, an ein Programm das du mit popen aufrufst und seinen Output einließt. Auf diese weise kann dein Filterprogramm auch ein Script oder ein awk oder sonstwas sein ohne das du für den Filter das Rad neu erfinden mußt.

Ich halte das aus verschiedenen Gründen für eine schlechte Idee: zum einen sind die Inhalte dann öffentlich sichtbar. Zum anderen muss man die Datei sauber wieder abräumen (gut, das bekommt man durch tempfile() hin). Desweiteren verbraucht man ggf. deutlich mehr Platz als nötig, denn mit einer temporären Datei muss man den kompletten Output erzeugen, bevor der andere Prozess ihn lesen kann - das gleiche gilt für den Rückweg. Außerdem ist Dateisystem IO teurer als IO im Hauptspeicher.

Nein, FIFOs sind schon der richtige Mechanismus. Man muss allerdings aufpassen, dass man beim parallelen Schreiben und Lesen kein Deadlock erzeugt. Da muss man dann Threads oder nonblocking IO verwenden. Zutaten:

  • mkfifo() oder pipe()

  • fork()

  • exec()

  • close() auf der schreibenden Seite nicht vergessen, nachdem man die letzten Daten geschickt hat

Ich habe auf die Schnelle das gefunden. Das kommt Deinem Anwendungsfall schon recht nahe, soweit ich das sehe.

microft

Avatar von microft

Anmeldungsdatum:
6. August 2009

Beiträge: 454

Wohnort: Norddeutschland

rklm schrieb:

Ich halte das aus verschiedenen Gründen für eine schlechte Idee: zum einen sind die Inhalte dann öffentlich sichtbar. Zum anderen muss man die Datei sauber wieder abräumen (gut, das bekommt man durch tempfile() hin). Desweiteren verbraucht man ggf. deutlich mehr Platz als nötig, denn mit einer temporären Datei muss man den kompletten Output erzeugen, bevor der andere Prozess ihn lesen kann - das gleiche gilt für den Rückweg. Außerdem ist Dateisystem IO teurer als IO im Hauptspeicher.

Er zieht seine Daten per cut/past rein. Bevor der Anwender die Taste losgelassen hat ist die Tempdatei längst geschrieben. Einen File zu löschen der als Parameter übergeben wurde, ist in der Tat ein größes Problem 😉 Auf heutigen Systemen mit Gigs an Hauptspeicher und Teras an Plattenplatz spielt der Platzverbrauch keine Rolle.

Das entscheidende Argument für meine Lösung ist aber die Flexibilität. Nochmal das Filterprogramm kann alles mögliche sein sogar ein Script ein AWK oder sonst was sein. Das Rad muss nicht immer neu erfunden werden.

Nein, FIFOs sind schon der richtige Mechanismus. Man muss allerdings aufpassen, dass man beim parallelen Schreiben und Lesen kein Deadlock erzeugt. Da muss man dann Threads oder nonblocking IO verwenden. Zutaten:

  • mkfifo() oder pipe()

  • fork()

  • exec()

  • close() auf der schreibenden Seite nicht vergessen, nachdem man die letzten Daten geschickt hat

Ich habe auf die Schnelle das gefunden. Das kommt Deinem Anwendungsfall schon recht nahe, soweit ich das sehe.

Irgend wie hab ich den eindruck das hier immer nach der kompliziertesten Lösung gesucht wird.

cu

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6518

Wohnort: Hamburg

Sorry, ich musste meine Antwort komplett neu schreiben, weil das System diese mehrfach dargestellt hatte (in der Vorschau) und jetzt habe ich festgestellt das bereits weitere Antworten eingetroffen sind. Ich kopiere meine ursprüngliche Antwort daher nochmal.

Ich habe jetzt etwas nachgedacht, und die Sache mit den ausgelagerten Tabellen eigentlich erstmal wieder verworfen. Da müsste ich ja auch zumindest eine Plausibilitätsprüfung veranstalten.

Schreib deine Eingansdaten einfach in eine temporäre Datei und übergebe diesen File dann, als Parameter, an ein Programm das du mit popen aufrufst und seinen Output einließt.

Wenn ich nichts besseres finde, wird es wohl darauf hinauslaufen. Aber da man mir an anderer Stelle empfohlen hat mich weiter zu entwickeln, überlege ich ob das nicht ein Anlass ist, mich näher mit Interprozesskommunikation zu beschäftigen. Ich hatte bisher nicht erwähnt, das ich für die GUI FLTK verwende. Auf einer Webseite dazu habe ich folgende interessante Links gefunden (alles die gleiche lange Seite):

Die Mathode Fl::add_fd() in den Beispielen scheint irgendwie auf select() zu basieren. Ob und wieweit man damit das Problem der Blockade bei bidirektionalen Pipe Verbindungen lösen kann, weiss ich aber (noch) nicht. Ich muss da nochmal drauf nachdenken. Bisher habe ich noch keine Beispiele für bidirektionale Kommunikation gefunden, nur Hinweise das sowas prinzipiell geht.

Die weiteren Posts lassen mich jedenfalls vermuten das es hier auch um Grundsatzfragen geht, so das es durchaus sinnvoll erscheint die Möglichkeiten (im Test) mal auszuloten.

Er zieht seine Daten per cut/past rein. Bevor der Anwender die Taste losgelassen hat ist die Tempdatei längst geschrieben.

Das ist in der Tat richtig. Selbst bei "suboptimaler" Programmierung (Funktionalität vorausgesetzt) ist der Zeitgewinn in Zehnerpotenzen auszudrücken. Alles was ein Programm macht ist schneller als der User die Tasten drücken kann.

Einen File zu löschen der als Parameter übergeben wurde, ist in der Tat ein größes Problem

Eigentlich auch nicht wirklich, denn das sollte ja der "Auftraggeber" machen wenn der Auftrag abgearbeitet wurde (und nichts blockiert, darüber habe ich aber noch nicht nachgedacht).

Vielen Dank erstmal für die bisherigen Tips. Ich muss darüber nochmal nachdenken. Mal sehen wie das morgen Nachmittag aussieht (der Vormittag ist ausgebucht ...).

p.s. Für die betreffende Datenmenge würde ich 4kB als Obergrenze ansehen. Tatsächlich lag die größte Datenmenge bisher nur knapp über 1024 Bytes. Jedenfalls weiss der "Auftraggeber" wie groß die tatsächliche Input Menge ist und kann für die Rückgabe eine entsprechende Reserve vorsehen. Allerdings kann das zugehörige Editor Objekt das auch selber handlen sofern das externe Programm nicht alles in einem riesigen MB-Stück zurückliefert, also Zeilenweise arbeitet.

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13219

microft schrieb:

rklm schrieb:

Einen File zu löschen der als Parameter übergeben wurde, ist in der Tat ein größes Problem 😉

Das nicht, aber man möchte es ja so haben, dass die Datei auf jeden Fall verschwindet, wenn der Prozess terminiert. Das sollte tempfile() leisten.

Auf heutigen Systemen mit Gigs an Hauptspeicher und Teras an Plattenplatz spielt der Platzverbrauch keine Rolle.

Ja, aber Platten-IO ist immer noch teurer als Hauptspeicher-IO.

Das entscheidende Argument für meine Lösung ist aber die Flexibilität. Nochmal das Filterprogramm kann alles mögliche sein sogar ein Script ein AWK oder sonst was sein. Das Rad muss nicht immer neu erfunden werden.

Ja und? awk und Co. lesen doch auch von der Standardeingabe und schreiben auf die Standardausgabe. Das ist normal. tr z.B. list sogar nur von Stdin und schreibt auf Stdout. Das ist doch der Kern der Unix-Philosophie.

Irgend wie hab ich den eindruck das hier immer nach der kompliziertesten Lösung gesucht wird.

fork() und exec() bzw. clone() brauchst Du auf jeden Fall, wenn Du einen Prozess starten willst.

Dakuan schrieb:

p.s. Für die betreffende Datenmenge würde ich 4kB als Obergrenze ansehen. Tatsächlich lag die größte Datenmenge bisher nur knapp über 1024 Bytes. Jedenfalls weiss der "Auftraggeber" wie groß die tatsächliche Input Menge ist und kann für die Rückgabe eine entsprechende Reserve vorsehen. Allerdings kann das zugehörige Editor Objekt das auch selber handlen sofern das externe Programm nicht alles in einem riesigen MB-Stück zurückliefert, also Zeilenweise arbeitet.

Da das Programm nicht kontrollieren kann, was der andere Prozess tut, musst es auf jeden Fall damit umgehen können, dass das Volumen deutlich höher liegt - und wenn es dann nur den FIFO schließt und nicht mehr weiter liest. Alles andere wäre instabil.

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6518

Wohnort: Hamburg

Zu der Variante "Datei als Parameter übergeben" habe ich noch folgendes gefunden:

    mkfifo ("telnet.out", 0600);
    out = popen ("/bin/telnet 10.0.0.3 1>telnet.out 2>/dev/null", "w");

Da wurde dann allerdings noch etwas mit Timeout und Puffergrößen rumgetrickst.

Ja, aber Platten-IO ist immer noch teurer als Hauptspeicher-IO.

Das bestärkt mich in der Annahme, das ich mich doch weiter mit dem Thema unnamed pipes beschäftigen sollte. Nach dem was ich bisher gelesen habe, muss ich dafür 2 Pipes einrichten.

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13219

Dakuan schrieb:

Zu der Variante "Datei als Parameter übergeben" habe ich noch folgendes gefunden:

    mkfifo ("telnet.out", 0600);
    out = popen ("/bin/telnet 10.0.0.3 1>telnet.out 2>/dev/null", "w");

Da wurde dann allerdings noch etwas mit Timeout und Puffergrößen rumgetrickst.

Ja, aber das ist nicht wirklich ein Vorteil gegenüber unbenannten FIFOs: Du musst immer noch die Eingabe füttern können (was popen() nicht leistet, wenn ich Dein früheres Posting richtig erinnere) und Du "sparst" lediglich, ein Umbiegen, nämlich von Stdin des Kindes auf das Leseende der Pipe. Dir bleibt aber erhalten, dass Du Schreiben und Lesen gleichzeitig erledigen musst.

Das bestärkt mich in der Annahme, das ich mich doch weiter mit dem Thema unnamed pipes beschäftigen sollte. Nach dem was ich bisher gelesen habe, muss ich dafür 2 Pipes einrichten.

Genau: eine, die den Prozess füttert, und eine andere, die das Ergebnis abholt.

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6518

Wohnort: Hamburg

Du musst immer noch die Eingabe füttern können (was popen() nicht leistet, wenn ich Dein früheres Posting richtig erinnere)

Das Problem mit popen() ist, das man entweder nur lesen oder nur schreiben kann. Die Idee in der Fundstelle war wohl die fehlende Richtung durch ein FIFO zu ersetzen und dieses im Aufruf per Umleitung anzusprechen. Aber das geht dann natürlich auch über die Platte.

Momentan kämpfe ich mit den gefundenen Beispielen, die ich auf meinen Anwendungsfall angepasst habe (momentan nur eine Richtung). ddd sagt etwas von Buffer-Underrun und dann verschwindet meine Anwendung im Nirwana (ddd sagt: normaly terminated). Ich vermute daher mehrere Fehler und muss das erstmal weiter eingrenzen.

An meinem Filter liegt es jedenfalls nicht, denn mit /usr/bin/less passiert genau das gleiche.

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6518

Wohnort: Hamburg

So, nach gefühlten 1000 Tipp- und Denkfehlern und einem entfernten Brett habe ich jetzt eine funktionierende Version. Da sind aber bestimmt noch einige Haken und Ösen drinn, die ich noch nicht erkannt habe. Und für die Kosmetik habe ich auch noch nichts getan (ausser das die Debug Meldungen raus sind).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int MainWindow::ext_filter( char * s, char * d )
{
    int     fd_pa2ch[2];
    int     fd_ch2pa[2];
    int     c;
    FILE    *fp_pa2ch, *fp_ch2pa;
    pid_t   pid;

    if( pipe( fd_pa2ch ) < 0 ) { /* create parent->child pipe */
        perror( "pipe1" );
        return -1;
    }
    if( pipe( fd_ch2pa ) < 0 ) { /* create child->parent pipe */
        perror( "pipe2" );
        return -1;
    }
    if( (pid = fork()) < 0 ) {
        perror( "fork" );
        return -1;
    }
    if( pid > 0 ) { /* parent --------------------------------------------- */
        close( fd_pa2ch[0] );
        close( fd_ch2pa[1] );
        if( (fp_pa2ch = fdopen( fd_pa2ch[1], "w" )) == NULL ) {
            perror( "fdopen1" );
            return -1;
        }
        if( (fp_ch2pa = fdopen( fd_ch2pa[0], "r" )) == NULL ) {
            perror( "fdopen2" );
            return -1;
        }
        while( 1 ) {
            fputc( *s, fp_pa2ch );
            if( *s++ == '\0' )
                break;
        }
        fclose( fp_pa2ch );
        while( !feof( fp_ch2pa ) ) {
            c = fgetc( fp_ch2pa );
            if( c > 0 )
                *d++ = (char)c;
        }
        *d = '\0';
        fclose( fp_ch2pa );
        if( waitpid( pid, NULL, 0 ) == -1 ) {
            exit( EXIT_FAILURE );
        }
    } else {    /* child --------------------------------- */
        close( fd_pa2ch[1] );   /* disable write direction */
        close( fd_ch2pa[0] );
        if( fd_pa2ch[0] != STDIN_FILENO ) {
            if( dup2( fd_pa2ch[0], STDIN_FILENO ) != STDIN_FILENO )
                exit( EXIT_FAILURE );
            close( fd_pa2ch[0] );
        }
        dup2( fd_ch2pa[1], STDOUT_FILENO );
        close( fd_ch2pa[1] );
        if( execl( "./filter", "filter", NULL ) != 0 ) {
            return -1;
        }
        exit( 0 );
    }
    return 0;
}

Was ich u.A. noch nicht verstanden habe sind die Zeilen 51 bis 55. In einem meiner Bücher steht das man es so machen muss, also das prüfen auf STDIN_FILENO. Im Internet finde ich aber meist eine vereinfachte Version, wie ich sie in den Zeilen 56 und 57 für die Gegenrichtung verwendet habe. Vielleicht kann mir jemand sagen, was es damit auf sich hat.

Das funktioniert also erstmal, solange keine Fehler passieren. Ich habe aber noch keine Ahnung, was ich im Fehlerfall mit den Filedisciptoren machen muss. Jedenfall soll im Fehlerfall das eigentliche Programm weiterlaufen, nur das der Text dann nicht modifiziert wird. Bei den gefundenen Beispielen wird immer gleich das ganze Programm beendet, was bei einer echten Anwendung nicht sein darf.

Und ja, die Pufferung der empfangenen Zeichen wird noch geändert.

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13219

Dakuan schrieb:

Was ich u.A. noch nicht verstanden habe sind die Zeilen 51 bis 55. In einem meiner Bücher steht das man es so machen muss, also das prüfen auf STDIN_FILENO. Im Internet finde ich aber meist eine vereinfachte Version, wie ich sie in den Zeilen 56 und 57 für die Gegenrichtung verwendet habe. Vielleicht kann mir jemand sagen, was es damit auf sich hat.

Wenn ich das auf die Schnelle richtig verstehe, dient der Test dazu, dass Du nicht einen Dateideskriptor auf sich selbst klonst, was einerseits keinen Sinn ergibt und andererseits ggf. schaden kann.

Dakuan

(Themenstarter)
Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6518

Wohnort: Hamburg

Ich dachte immer, das so etwas nicht passieren kann. Aber in diesem Zusammenhang fällt mir auf, dass die Beispiele mit Überprüfung Konsolen Anwendungen sind und die ohne mit GUI.

Ich kämpfe aber noch mit der Fehlerbehandlung. Wenn der Anwender z.B. einen falschen Programmnamen eingibt, oder das Filterprogramm während der Laufzeit des Hauptprogramms "abhanden kommt", bleibt das Programm hängen. Es scheint so, das execl() dann mit Ergebnis 0 oder gar nicht terminiert. Debuggen kann ich das nicht richtig, da gdb/ddd immer im Parent Prozess bleibt.

In der Konsole kommt dann folgendes:

pdb: ../../src/xcb_io.c:182: process_responses: Assertion `((int) (((dpy->last_request_read)) - ((dpy->request))) <= 0)' failed.
Aborted

womit ich nichts anfangen kann. (ja, der Programmname ist unglücklich gewählt, es ist nicht der Python Debugger). Das Hauptprogramm lässt sich aber noch bedienen und korrekt beenden.

Aber der Thread im Hauptprogramm bleibt wohl irgendwo hängen. Bei jedem Aufruf des Filters entsteht im Fehlerfall ein neuer Prozess der nicht beendet wird. Diese verschwinden erst, wenn das Hauptprogramm beendet wird.

@qube:~$ ps -a
  PID TTY          TIME CMD
 6508 pts/2    00:02:33 htop
 7394 pts/0    00:00:00 pdb
 7395 pts/0    00:00:00 pdb
 7408 pts/0    00:00:00 pdb
 7409 pts/3    00:00:00 ps
@qube:~$ ps -a
  PID TTY          TIME CMD
 6508 pts/2    00:02:37 htop
 7416 pts/3    00:00:00 ps
@qube:~$ 

Irgendwie benötige ich eine andere Fehlerbehandlung. Am besten währe eine Zeitüberwachung. Gibt es dazu "übliche" Vorgehensweisen?

Antworten |