Warning: include_once(/var/www/html/pmwiki-2.2.86/cookbook/soap4pmwiki/soap4pmwiki.php): failed to open stream: No such file or directory in /var/www/html/fields/dbp15/local/config.php on line 4

Warning: include_once(): Failed opening '/var/www/html/pmwiki-2.2.86/cookbook/soap4pmwiki/soap4pmwiki.php' for inclusion (include_path='.:/opt/php/lib/php') in /var/www/html/fields/dbp15/local/config.php on line 4

Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/fields/dbp15/local/config.php:4) in /var/www/html/pmwiki-2.2.86/pmwiki.php on line 1250
Datenbankpraktikum SS 2015 - D - Consoleclient

Konsolenclient

Der neben dem Webinterface entwickelte Konsolenclient ermöglicht die Interaktion mit dem Server über ein Linux-Terminal und ist in der Lage, die erhaltenen Daten in tabellarischer Form darzustellen. Implementiert wurden alle bereits verfügbaren Funktionen der uosql-Datenbank.

Startup und Login

Der Start des Clienten über das Terminal kann durch eine Anzahl Parameter beschleunigt werden. Vorgesehen ist die optionale Angabe einer IP-Adresse, eines Ports sowie von Nutzername und Passwort über die Befehle "--bind "<IP>, "--port <Port>", "--name <Nutzername>" und "--pwd <Passwort>", wobei die Reihenfolge der Nennung beliebig freigestellt ist. Nicht angegebene Parameter werden beim Programmstart auf der Kommandozeile abgefragt. Wird auf die Eingabe einer IP-Adresse oder eines Ports auch bei Nachfrage durch den Clienten durch Eingabe von "Return" verzichtet, nutzt der Client den jeweiligen Default-Wert (127:0.0.1:4242); die Abfrage der Login-Daten erfolgt theoretisch in einer Endlosschleife bis ein Nutzer authentifiziert werden konnte. Da die Implementierung einer echten Authentifizierung aus Zeitgründen nicht möglich war, wird, mit Ausnahme von Leereingaben, in der aktuellen Server-Version eine beliebige Kombination von Nutzername und Passwort als gültiger Benutzer akzeptiert.

Client-Funktionen

Basisfunktionen des Clienten werden durch einen vorangestellten Doppelpunkt markiert und von der Syntax üblicher SQL-Statments getrennt. Dabei fragt der Client solange in einer Endlosschleife nach Eingaben, bis eine Funktion durch einen Boolean-Wert einen kritischen Fehler anzeigt, der eine Fortsetzung des Programms nicht ratsam erscheinen lässt. Darunter fällt etwa das Auftreten eines generischen Fehlerfalls, der nicht den durch die Gruppe erstellten und definierten Fehlerfällen entspricht. Implementiert wurden außerdem eine Anzahl grundlegender Funktionen in Anlehnung an Pachev 20071. Der Client erlaubt es, über den Befehl ":ping" abzufragen, ob der Server noch aktiv ist und reagiert sowie das Programm auf zwei Arten zu beenden. Über die Eingabe ":quit" wird ein Quit-Package an den Server geschickt, der Client aber nur beendet, wenn der Server mit einem OK-Paket reagiert. ":exit" schickt ebenfalls ein Quit-Package, terminiert die Client-Anwendung aber unabhängig von der Antwort des Servers. Sämtliche Funktionen des Clienten sind in der Readme.txt dokumentiert, deren Aufruf aus dem Clienten heraus über den Befehl ":help" möglich ist. Die Ausgabe der vorformatierten Hilfe-Datei erfolgt auf der Kommandozeile.

SQL-Script

Darüber hinaus besteht die Möglichkeit, SQL-Scripte aus Dateien zu laden. Der Befehl ":load" lädt die bereits angelegte und mit einem kleinen Tutorial gefüllte Datei script.sql aus dem Stammverzeichnis des Clienten, was die schnelle Ausführung häufig wiederkehrender Befehlsketten ermöglicht. Wird ":load" mit einer Pfadangabe versehen, kann eine beliebige .sql-Datei aus einem beliebigen Verzeichnis geladen werden. Die Ausführung unterbricht, sobald ein Befehl nicht erfolgreich ausgeführt werden kann, was der Client anhand des übermittelten Error-Pakets erkennt, da in diesem Fall kein sinnvoller Zielzustand der Datenbank erreicht werden kann. Unterstützt wird die übliche Syntax für SQL-Scripte, bei der einzelne Statments durch ein Semicolon getrennt werden. Kommentare lassen sich über (De-)Limitier "/*" und "*/", einzelne Zeilen können über "#" und "-- " markieren. Die Erkennung von SQL-Befehlen erfolgt durch nach Auslesen der gesamten Datei in einen einzelnen String, der anschließend zeichenweise betrachtet wird. Der Client merkt sich hier über Boolean-Werte, ob er sich in einer auskommentierten Zeile oder einem auskommentierten Bereich befindet und speichert mit Ausnahme von Zeilenumbrüchen alle übrigen Zeichen in einem neuen String. Dieser Ergebnisstring wird im Anschluss durch eine Funktion aus der Rust-Standardbibliothek an den Trennsymbolen (";") in einzelne SQL-Befehle zerlegt, in einem Array gespeichert und sequentiell an den Server gesendet.

SQL-Befehle

Alle Eingaben, die nicht als Client-Befehl über die Match-Funktion von Rust identifiziert werden können, werden als SQL-Statement behandelt und als Query an den Server gesendet. Der Client-Befehlen vorangestellte Doppelpunkt dient lediglich als Unterscheidungsmerkmal zur SQL-Syntax. Das Abschicken falsch geschriebener Client-Befehle wird jedoch nicht verhindert, da das Matching zwar case-insensitive, aber exakt erfolgt. Die Bewertung nicht abgefangener Eingaben obliegt vollständig dem Parsing-Modul des Servers. Über möglicherweise aufgetretene Fehler sowohl im Bereich der Client-Server-Kommunikation als auch von SQL-Statements wird der Anwender durch Error-Pakete informiert, in denen eine Fehlermeldung als String übertragen wird.

Darstellung und Ausgabe

Die Ausgabe der Datenbank-Abfrage erfolgt automatisch, sofern ein DataSet übermittelt wird. Enthält das DataSet keine Daten sondern nur eine Tabelle, werden die Metadaten ausgegeben, andernfalls die Daten selbst in tabellarischer Form. Die maximale Breite einer Spalte wird dynamisch angepasst und bewegt sich in einem Intervall, dessen untere Grenze von dem maximal für den angegebenen Datentyp nötige Größe bestimmt wird. Für einen Boolean-Wert werden also fünf Zeichen vorausgesetzt. Die maximale Breite einer Spalte ist fest auf 30 Zeichen limitiert, die praktische Größe orientiert sich daher an der Breite des Spaltennamens. Ist eine Ausgabe länger als 30 Zeichen, wird die Darstellung nach 27 Symbolen abgebrochen und bis zum Zeichenlimit mit Punkten aufgefüllt, um die Kürzung anzuzeigen. Die Implementierung findet damit eine Balance zwischen effizienter Nutzung der Terminalbreite und Laufzeit. Die Bestimmung der tatsächlich nötigen Spaltenbreite über den größten Einzeleintrag wurde verworfen, um nicht ein zusätzliches Mal durch das vollständige ResultSet iterieren zu müssen.

Easter Eggs

Besondere, der Erheiterung oder Entspannung des Nutzers dienende Funktionen können über nicht in der Readme-Datei dokumentierte Befehle aufgerufen werden. Die Eingabe ":hello" gibt eine augenzwinkernde Begrüßung auf der Konsole aus, welche die AI HAL 9000 aus dem Film 2001: A Space Odyssey (1968) zitiert. Die Befehle ":snake" und ":insult" starten die Videospiel-Klassiker Snake und Space Invaders. Beide Spiele werden vollständig im Terminal ausgeführt und erlauben die nahtlose Fortsetzung der Client-Server-Interaktion nach Ende einer Partie. Der Code stammt aus dem Github-Repository Rust-Examples von Johannes Schickling und wurde unter MIT-Lizenz veröffentlicht, weshalb sich die Verwendung und Änderung des Codes im Rahmen dieses Projektes in einem rechtlich legalen Rahmen bewegt. Während des Praktikums wurde der noch mit einer Beta-Version von Rust geschriebene Code aktualisiert. Dabei wurden primär durch geänderte Syntax sowie Legacy-Funktionen hervorgerufene Compiler-Fehler behoben und der Code in einer einzelnen Datei (specialcrate.rs) zusammengeführt. Die aktualisierte Fassung ist dem Quell-Repository zur Verfügung gestellt und zwischenzeitlich in das Repository integriert worden.

Command-Historie zur Nutzerunterstützung

Dieser Absatz wurde geschrieben von: Svantje Jung

Von der Konsole, die bei diversen Betriebssystemen Standard ist, ist bekannt, dass beim Drücken der Pfeiltasten nach oben und unten die Einträge der Kommando-Historie erscheinen und mithilfe der Pfeiltasten zwischen den Einträgen ausgewählt werden kann. Dieses Verhalten sollte für unseren Kommandozeilen-Client nachgebaut werden, denn standardmäßig ist die Konsole beim Programmstart in einem Zustand, der die Tastendrücke direkt an den Standard-Inputstrom weiterleitet, sodass auf Tastendrücke von Sondertasten, wie den Pfeiltasten, nicht vernünftig reagiert werden kann. Mehr noch werden in dem genannten Zustand bzw. Eingabemodus die meisten Sondertasten mit Zeichenketten direkt auf die Konsole ausgegeben. Wird die Pfeiltaste nach oben gedrückt, so wird auf der Konsole "^[[A" angezeigt, ähnliches gilt für andere Sondertasten. Damit wir die Pfeiltasten registrieren können und zum Teil mit besonderen Funktionen bestücken können, muss also der Eingabemodus der Konsole im Programm geändert werden. Dafür wird der sogenannte Raw-Input-Modus für die Konsole benötigt, sowie die Funktionalität die Tastatureingaben zu filtern. Es existieren bereits Rust-Bibliotheken, die Tastaturschnittstellen bereitstellen, allerdings haben diese Bibliotheken einen starken Überhang an benötigten Abhängigkeiten zu anderen Bibliotheken bzw. einen immensen Überhang an Funktionen. Da wir den Einsatz von Bibliotheken möglichst gering halten wollten, wurde über kompaktere Alternativen nachgedacht. Da wir keine Möglichkeit für Rust gefunden haben über eine Schnittstelle direkt auf die Konsoleneinstellungen zuzugreifen, haben Recherchen die Möglichkeit des Aufrufs externer, in anderen Programmiersprachen geschriebenen, Funktionen in Rust aufgezeigt (Stichwort: Foreign Function Interface (FFI)). Dadurch wurde es ermöglicht aus Rust eine in C geschriebene Funktion, die die Pfeiltasten (Oben/Unten) abfängt, einzubinden.

Die in C geschriebene Funktion folgt der in folgendem Pseudocode beschriebenen Logik.

int key(void)
{
	// prepare terminal for raw-input
	terminal_init(); 
 
	// read character from stdin
	int c = getc(stdin);
	int val = -1;
	if (c == ARROW_UP) val = 0;
	else if (c == ARROW_DOWN) val = 1;
	else val = c;
 
	// restore original terminal state
	terminal_done();
	return val;
}

Um die Konsole in den Raw-Input-Modus zu setzen wird die Funktion terminal_init() aufgerufen, die sich in der Header-Datei "minimal.h" befindet. Diese Datei wurde von einem Foreneintrag über Beispiele zu Termios übernommen (mit einer kleinen Codekorrektur - siehe Zeile 22: minimal.h). Damit am Ende der Originalzustand der Konsole wieder hergestellt wird, muss am Ende die Funktion terminal_done(), die sich ebenfalls in "minimal.h" befindet, aufgerufen werden. Dazwischen wird auf die Tasten, die für die Funktionalität von Interesse sind, geprüft. Im Falle der Pfeiltaste nach oben wird eine 0 zurückgegeben, bei der Pfeiltaste nach unten eine 1, im Fehlerfall eine -1 und ansonsten der Wert, den die gedrückte Taste hat.

Um diese Funktion schließlich nutzen zu können, mussten noch einige Anpassungen an das Projekt gemacht werden. Diese sollen im Folgenden kurz beschrieben werden. In der Cargo.toml, dem Buildfile unseres Projekts, musste folgendes eingefügt werden

  • unter dem Tag [package] die Zeile:
    • build = "src/client/native/build.rs"
  • unter dem Tag [dependencies] die Zeile:
    • libc = "0.1.10"
  • unter dem Tag [build-dependencies] die Zeile:
    • gcc = "0.3"

Damit die C-Funktion auch kompiliert wird, musste anschließend noch eine "build.rs" erstellt werden, über die schließlich der externe Code kompiliert wird. Zu finden ist die Datei in unserem Projekt unter dem Pfad /src/client/native/build.rs.

Die momentane Umsetzung dieser C-Einbindung hat natürlich ihre Schwächen, die im weiteren Entwicklungsverlauf wünschenswerterweise noch behoben werden sollten. Dadurch dass die Funktion extern aufgerufen wird und nur anhand eines Characters den weiteren Programmverlauf beeinflusst, wird sich im Falle einer Nicht-Pfeiltaste gemerkt, welche Taste stattdessen gedrückt wurde. So muss der Benutzer diese Taste nicht erneut drücken, wenn das Programm in die Behandlung dieses Falls übergeht. Deshalb wird dann die gespeicherte Taste zunächst auf die Konsole ausgegeben, bevor der Nutzer nach der weiteren Eingabe gefragt wird. Dieses hat momentan den entscheidenden Nachteil, dass sich das erste Zeichen nicht löschen lässt, da es manuell, über den Standard-Output-Stream, hinzugefügt wurde. Um dieses zu beheben müsste dieses Zeichen wahrscheinlich, sofern dieses möglich ist, in denselben Stream gepackt werden, wie die restliche Eingabe, d.h. in den Standard-Input-Stream. Diese Idee wurde bisher allerdings nicht ausgetestet.

Eine weitere Schwachstelle ist, dass die History nicht veränderbar ist, d.h. dass die Einträge, die aus der History angezeigt werden, nicht angepasst werden können, falls man sich zuvor beispielsweise vertippt hat. Auch hier liegt die Vermutung, dass auf den falschen Stream geschrieben wird, sodass der Nutzer leider in dem Zustand keine Änderungen vornehmen kann. Um das zu beheben würde zunächst die gleiche Idee wie bereits oben genannt ausgetestet werden.

Damit die Kommando-Historie auch über Programmstarts hinaus nutzbar wird, wurden die Einträge der Datenstruktur, in der die Einträge gehalten wurden, serialisiert und in eine Datei "uosql_client.historie" gespeichert. Bei einem gültigen Programmende werden nun diese Daten auf die Festplatte geschrieben und bei einem erneuten Programmstart versucht wieder auszulesen. Damit die Historie einsehbar wird, wurde eine zusätzliche Funktion eingebaut: ":log". Mithilfe dieser Funktion ist es möglich sich alle Einträge der Historie anzeigen zu lassen. Um das ganze Benutzerfreundlicher gestalten zu können, würde sich anbieten hinter dem ":log" einen Parameter n zu erlauben, der einem nur die ersten/ letzten n Einträge der Historie anzeigt. Es wäre ebenfalls wünschenswert zu verbessern, dass die Einträge nicht in einem Vektor gespeichert werden, sondern in einer HashMap-ähnlichen-Datenstruktur (Datenstruktur mit Key-Value-Paaren). Dadurch könnte gewährleistet werden, dass sich jedes Kommando nur einmal in der Datenstruktur befindet (Key) und über den Value könnte beispielsweise die Position in der Historie simuliert werden. Damit das Update der Values effizient passiert, müsste sich weiter über eine geeignete (und möglichst in Rust bereits existierende) Datenstruktur informiert werden.


Autor: Max Doll, Svantje Jung (Absatz: Command-Historie zur Nutzerunterstützung)
Gruppe: Max Doll, Svantje Jung, Elena Resch


1 Sascha Pachev: Understanding MySQL Internals. Sebastopol 2007, S69ff.


Page last modified on September 21, 2015, at 03:08 PM