Im zweiten Teil der Serie haben wir beschlossen eine Alternative zum fantastischen node-itoolkit zu erstellen. Dazu haben wir unser Projekt xmlservice-ts und ein Beispielprojekt eingerichtet, welches als Prüfstand dienen wird. Zusätzlich haben wir das xmlservice-ts NPM-Paket erfolgreich veröffentlicht. In dem aktuellen Teil der Serie werden wir das sogenannte „child process“ Modul von Node.js verwenden, um xmlservice-cli aufzurufen.
Die Entwicklungsumgebung ist wie folgt aufgebaut:
xmlservice-cli ohne jegliche Argumente aufrufen
Im ersten Teil dieser Serie haben wir einige Besonderheiten von xmlservice-cli kennen gelernt. Eine interessante Eigenschaft ist die Tatsache, dass xmlservice-cli nicht nur typische Befehlszeilenparameter im Bindestrich-Format erwartet, sondern einen Eingabe-Datenstrom über STDIN (Standard Input).
Ziel ist es also xmlservice-cli erstmal überhaupt aufzurufen und über STDIN das „HELLOKERIM“ (das PGM tut im Grunde nichts und dient lediglich als Dummyaufruf) Programm per folgendem XML auszuführen.
<?xml version=’1.0′?>
<script>
<pgm name=’HELLOKERIM’ lib=’GUENEY’>
</pgm>
</script>
Natürlich soll xmlservice-ts als eine schicke Abstraktionsschicht aufrufbar sein wie z. B. so:
Wir sehen hier schon einige Features, die das node-itoolkit aktuell noch nicht unterstützt. Nämlich Unterstützung für Promises bzw. async/await und die Möglichkeit xmlservice-cli direkt mit XML zu steuern.
Aber nun ans Eingemachte. Für die Entwicklung der Connection-Klasse, erstellen wir im xmlservice-ts Projekt einen Git-Branch namens „run-xmlservice-on-local-system“. In Visual Studio Code geht das sehr einfach so, indem man unten auf den „main“ Branch klickt und in der Befehlsauswahl „Create new branch“ ausführt:
Danach erstellen wir eine minimale Connection-Klasse in der gleichnamigen „Connection.ts“-Datei:
Und löschen den markierten Platzhalter-Code und exportieren stattdessen unsere neue Klasse mit:
export * from ‘./Connection’;
Damit wir die Klasse später mit
const { Connection } = require(“xmlservice-ts”);
laden und verwenden können.
Nun können wir „Connection.ts“ bearbeiten. Als erstes laden wir natürlich die „spawn“-Funktion aus „child_processes“ und definieren wir zwei Interfaces. Ein Interface für den Konstruktor der Klasse und ein Interface, welches unser Ausgabeobjekt definieren wird:
import { spawn } from ‘child_process’;
export interface ConnectionConfig {
// falls das xmlservice-cli nicht unter /QOpenSys/pkgs/bin/
xmlservicePath?: string;
}
export interface XmlserviceResult {
output: string | null; // xmlservice-cli output
signal: string | null; // signal des Prozesses
code: number | null; // Rückgabewert des Prozesses (0 = OK)
}
Somit sieht unsere Klasse schon mal so aus:
export class Connection {
#xmlservicePath: string;
// optionaler Konfigurationsparameter
constructor(config?: ConnectionConfig) {
this.#xmlservicePath = config?.xmlservicePath || ‘/QOpenSys/pkgs/bin/xmlservice-cli’;
}
}
Einige werden sich hier wundern, was das Feld namens „#xmlservicePath“ sein soll. Im aktuellen ECMA-Script-Standard werden private Klassenvariablen mit einer vorangehenden Raute identifiziert. TypeScript ermöglicht es aber auch weiterhin das „private“-Keyword zu verwenden.
Die execute-Methode: asynchrone Ausführung mit Promises
Nun möchten wir eine Methode definieren, womit wir xmlservice-cli aufrufen und mit unserem XML steuern können. Die Methode soll vor allem ein Promise zurückgeben, so dass das Aufrufende Programm den Kontrollfluss mit async/await steuern kann. Die asynchronen Funktionen im „child_process“-Modul verwenden aktuell leider immer noch Events mit Callbacks. D. h. wir werden die Events manuell zu Promises umwandeln – auch „promisifying“ genannt.
Die Erklärungen für die mit Kommentarzeilen markierten Stellen stehen weiter unten.
execute(xmlIn?: string): Promise<XmlserviceResult> {
// 1) gibt eine Promise zurück, die aufgelöst wird, wenn xmlservice-cli fertig ist
return new Promise((resolve, reject) => {
// 2) xmlIn in einen Buffer umwandeln
const inputBuffer = xmlIn ? Buffer.from(xmlIn) : null;
const outputBuffer: Buffer[] = [];
const result: XmlserviceResult = {
output: null,
signal: null,
code: null,
};
// 3) xmlservice-cli starten
const xmlservice = spawn(this.#xmlservicePath);
// 4) wenn xmlservice-cli etwas auf stdout schreibt, wird das in den outputBuffer gepusht
xmlservice.stdout.on(‘data’, (chunk) => {
outputBuffer.push(chunk);
});
// 5) wenn der gespawnte Prozess beendet wird, wird die Promise aufgelöst
xmlservice.on(‘close’, (code, signal) => {
// alle Teile des outputBuffers zusammenfügen und in einen String umwandeln
result.output = Buffer.concat(outputBuffer).toString();
// Ausführungscode und Signal des Prozesses speichern
result.code = code;
result.signal = signal;
// wenn der Prozess mit Code 0 beendet wurde, wird die Promise aufgelöst
if (code === 0) {
resolve(result);
} else {
// sonst wird die Promise abgelehnt
reject(result);
}
});
if (xmlIn) {
// 6) wenn xmlIn übergeben wurde, wird es an xmlservice-cli als Buffer im stdin übergeben
xmlservice.stdin.write(inputBuffer);
xmlservice.stdin.end();
}
});
}
Wow. Auf den ersten Blick sieht das ungeheuer kompliziert aus, aber wir werden sehen, dass es eigentlich ganz einfach ist. Wir gehen die einzelnen Punkte durch:
- Hier erstellen wir ein neues Promise-Objekt, welches an den Aufrufer zurückgegeben wird. Der Promise-Konstruktor erwartet ein Callback mit zwei Parametern. Diese Parameter werden mit Funktionen befüllt, die das Promise erfolgreich erfüllen (resolve) oder ablehnen (reject) lassen.
In diesem Callback können wir nun beliebige asynchrone Sachen tätigen und dann zum richtigen Zeitpunkt das Promise abschließen. - Da wir unser XML in das STDIN des xmlservice-cli schieben wollen, wandeln wir es hier zu einem Buffer um.
- Hier starten wir nun xmlservice-cli! Die „spawn“-Funktion gibt uns ein Prozessobjekt, welches nun auf Eingabe/Ausgabe-Events horchen und den Prozess steuern kann. Der xmlservice-cli ist zu diesem Zeitpunkt gestartet und wartet auf unseren Input.
- Bevor wir dem Prozess unseren Input schicken, definieren wir per „on“-Methode, wie wir auf die Antwortevents reagieren wollen. Wir sammeln die STDOUT-Antwort des Prozesses, welches ein Stream ist, als Bufferstücke.
- Wenn der Prozess sich schließt, platzieren wir alle gebufferten Stücke als einen langen String in unser Outputobjekt.
- Punkt 4 und 5 haben nur definiert, was passieren soll, wenn xmlservice-cli antwortet. Aktuell wartet es aber immer noch. Hier schicken wir nun endlich unser XML an STDIN und beenden unsere Eingabe.
Ergebnis
Nachdem wir unser Projekt kompiliert haben, können wir unseren Testcode aus dem ersten Screenshot ausführen. Das Ergebnis sieht so aus:
result {
output: “<?xml version=’1.0′?>\n” +
‘ <script>\n’ +
” <pgm name=’HELLOKERIM’ lib=’GUENEY’>\n” +
‘<success><![CDATA[+++ success GUENEY HELLOKERIM ]]></success>\n’ +
‘</pgm>\n’ +
‘</script>\n’,
signal: null,
code: 0
}
Sehr schön! Wir haben erfolgreich xmlservice-cli aus Node.js aufgerufen! Der aufmerksame Leser wird gemerkt haben, dass dieser Aufruf aber nur zustandslos ist. Im nächsten Teil der Serie werden wir die Connection-Klasse erweitern, so dass auch zustandsvolle Aufrufe möglich werden.
Der Autor Kerim Güney schreibt regelmäßig für den TechKnowLetter.
Sie erreichen ihn unter:
kerim[at]gueney.io
Sechs Ausgaben des TechKnowLetters erhalten Sie hier für 88 Euro.