In diesem Beitrag wird eine sinnvolle Anwendung für Node.js auf dem lokalen Rechner dargestellt: eine Web-Browser gestützte Console mit GUI für die Steuerung von Automatisierungsaufgaben, wie sie beispielsweise im Zusammenhang mit Staging & Deployment, Maintenance und Continous Integration für die Web-Portal-Entwicklung typisch sind.
Viele weitere Anwendungsbereiche sind denkbar: Node.js-GUI's für
- Staging & Deployment, Continous Integration, Maintenance für die Webentwicklung (hatten wir schon, der Vollständigkeit halber hier nochmal),
- Medien (Heimkino, Media-Center),
- Synchronisation von Daten (lokal, LAN, WLAN, remote, Cloud, Firma, Kunden, Arbeitsplatz),
- Steuerung von Hardware, z.B. Arduino (Schule, Ausbildung, Hobby, CNC-Werkzeug-Steuerung),
- Steuerung von Haustechnik bzw. Internet der Dinge (Heizung, Strom, Energiemanagement, Licht, Alarm, Überwachung, Garten-/Balkonbewässerung).
In Folgenden wird nur eine Lösung für den ersten Punkt oben (Staging & Deployment, Continous Integration, ...) vorgestellt. Dieser wird zudem nicht vollständig ausgebaut, sondern so ausführlich beschrieben, dass jeder leicht seine eigenen Grunt-Aufgaben hinzufügen kann. Material als Anregung hierfür gibt es in diesem Weblog mittlerweile reichlich.
Ich z.B. habe für mein Portalmanagement die folgende GUI entwickelt (vgl. Abb. 1), weil ich dringend so etwas benötigte. Damit macht Entwickeln wieder Spaß, weil ich keine lästigen Consolen-Befehle mehr während des Entwicklungsprozesses ausführen muss, sondern einfach nur noch eine Aktion auswähle (z.B. Drupal upgraden oder Portal-Backup), ein paar Zusatz-Spezifikationen ebenfalls auswähle (Quelle, Ziel, Datenbank-Prefix) und dann nur noch den Ausführen-Button betätige.
Einige der Tasks stellen Rückfragen, ob sie zum Beispiel irgendwo etwas kritisches Löschen dürfen. Dafür benötige ich die Console.
Dies stellt eine enorme Steigerung meiner Produktivität dar gegenüber meiner vorherigen Arbeitsweise mit den vielen Windows-Konsolen, die ich gleichzeitig offen hatte.
Abb. 1: Meine Node.js/Grunt-GUI für mein Portal-Management
Gestartet werden über Aktionen hauptsächlich Grunt-Skripte. Ich werde deshalb im Folgenden nur die Auswahl eines Test-Grunt-Skripts zulassen, welches allerdings eine Eingabe per Console simuliert, um den Lösungsansatz komplett darzustellen.
Der Eingabe-Prompt des Test-Grunt-Skripts wird benötigt, um zu zeigen, wie die Kommunikation in beide Richtungen über die CSS-Konsole (Abb. 1) zwischen dem Browser-Client und Node.js-Server funktioniert.
Die Implementierung komplexer eigener Tasks nach eigenen Vorstellungen dürfte dann für diejenigen, die vorher die Grunt-Artikel-Reihe gelesen haben, kein Problem sein.
Es macht keinen Sinn, mein eigenes System hier komplett zu veröffentlichen, da mein System viel zu spezfisch ist. Jeder muss eben seinen Workflow abbilden. Schwieriger, als die Parametrisierung einer Komplett-Lösung mit einem Komplett-System für z.B. die Continous Integration, ist die Implementierung eigener Grunt-Tasks in das vorgestellte System sicher nicht.
Anzumerken ist allerdings, dass das System noch nicht perfekt ist. Das bezieht sich aber hauptsächlich auf Schönheitsaspekte. Ich hatte bisher noch keine Zeit für den Feinschliff. Den praktischen Nutzen des Systems beeinträchtigt dies nicht.
Die Implementierung einer Log-Funktion wäre zudem praktisch. Dies dürfte aber eine ganz leichte Übung sein, wenn man das Folgende nachvollzogen hat.
Umsetzung
Für die im Weiteren beschriebene Umsetzung sollte man sich die folgende Projektstruktur vergegenwärtigen. Die Projektstruktur sieht so aus:
Projektordner - ansi_up - - ansi_up-Library - client - - assets - - - css - - - - styles.css - - - img - - - - icon-64.png - - - js - - - - cssConsole - - - - - alle Sourcen des OpenSource-Projekts cssConsole - - - - app.js - - index.html - node_modules - - express - - socket.io - - xml2json - server.js Grunt-Task-Ordner - node_modules - - grunt - Gruntfile.js - package.json
Aus dem Einstiegs-Artikel in Node.js kennen wir die Funktion von server.js mit dem Server-Skript und den Ordner Client, in dem unser Express-Modul von Node.js die GUI-Anwendung findet wird inklusive Bilder, CSS- und Client-Javascript-Dateien.
Wir benötigen aus dem Internet die folgenden OpenSource-Ressourcen, die wir - wie in der Struktur angegeben - speichern bzw. implementieren:
- ansi_up wandelt Console-Steuerungszeichen für Farbdarstellungen um in Html-Code und die
- cssConsole, deren Sourcen wir, wie in den nächsten Abschnitten beschrieben, in unsere Skripte einbauen.
Im Ordner node_modules erkennt man zwei Node.js-Module, die im Projektordner über eine Windows-Console installiert werden:
npm install express npm install socket.io npm install xl2json
Die oben angegebene Ordner- bzw. Datei-Struktur sollte sich jetzt einrichten können.
Natürlich sind die Dateien server.js, app.js und style.css noch leer und werden im Folgenden mit Inhalten zu füllen sein. Das 64-bit-große Icon kann sich jeder selbst basteln.
Struktur der Webseite: index.html, styles.css
Als erstes stelle ich die Html-Struktur von index.html vor. Das folgende Skript kopiert man sich den Client-Ordner (s. Projektstruktur) als index.html:
<!doctype html> <html> <head> <meta charset="utf-8"/> <title>StageAndDeploy:Grunt GUI für ein Drupal-Projekt</title> <link href="http://fonts.googleapis.com/css?family=Denk+One" rel="stylesheet" type="text/css" /> <link href="./assets/css/styles.css" rel="stylesheet" type="text/css" /> <link href="./assets/js/cssConsole/cssConsole.css" type="text/css" rel="stylesheet"> </head> <body> <header> <div class="container"> <div class="logo-header-main"><img src="./assets/img/icon-64.png" /></div> <div class="logo-text-main"> <h1>StageAndDeploy</h1> <h2>Grunt GUI für ein Drupal Projekt</h2> </div> <nav> <a href="#">Home</a> <a href="http://code-kiste.hauertmann.com/" target="_new">Blog</a> <a href="http://hauertmann.com/content/leistungen-und-kompetenzen" target="_new">Support</a> </nav> <div class="clear-fix"></div> </div> </header> <main> <div class="container"> <div id="form-action-container"> <section class="form-input"> <div class="form-entity"> <h5>Aktion:</h5> <select id="select-action"> <option value="none">Keine</option> <option value="test">(Test)</option> </select> </div> <div class="form-entity"> <h5> Aktion anwenden auf: </h5> <label for="select-target">Ziel</label> <select id="select-target"> <option value="local">Lokal XAMPP !</option> <option value="test">Test</option> <option value="staging">Staging</option> <option value="production">Production</option> </select> </div> <div class="form-entity"> <button id="button-submit">Ausführen</button> </div> </section> </div> <aside> <section class="form-output"> <div class="form-entity"> <h5>Ausgabe Konsole</h5> <textarea id="textarea-console"></textarea> <div id="cssConsole"> <div class="console"> <div class="line white">Welcome to cssConsole!</div> </div> <div class="bottom"> <div class="label">></div> <div id="input"></div> </div> </div> </div> </section> </aside> </div> <div class="clear-fix"></div> </main> <footer> <div class="container"> <div class="contact"> <h5>Kontakt</h5> <p>Wolfgang Hauertmann</p> <p class="important">~ Weblösungen ~</p> <p>Richrather Str. 196</p> <p>40723 Hilden</p> </div> <div class="copyright"> <p>(c) 2014 Wolfgang Hauertmann</p> </div> <div class="sitemap"> <h5>Sitemap</h5> <ul> <li><a href="#">Home</a></li> <li><a href="http://hauertmann.com" target="_new">Über mich</a></li> <li><a href="http://hauertmann.com/content/leistungen-und-kompetenzen" target="_new">Support</a></li> </ul> </div> </div> </footer> <script src="http://localhost:8000/socket.io/socket.io.js"></script> <script src="http://code.jquery.com/jquery-2.0.3.min.js"></script> <script src="./assets/js/cssConsole/cssConsole.min.js"></script> <script src="./assets/js/app.js"></script> </body>
Man erkennt im Skript, welche Ressourcen für das Projekt woher bezogen werden.
jQuery beispielsweise wird der Einfachheit halber aus dem Netz bezogen.
Was socket.io.js ist, wird weiter unten erläutert.
Die cssConsole wird eingebunden und natürlich das Client-Script app.js, das unsere GUI interaktiv macht.
Im Head befinden sich die Verweise auf die zugehörigen Styles zu den genannten Komponenten.
Damit es direkt schön aussieht, spendiere ich hier noch das zugehörige CSS-File styles.css, das im assets-Ordner unter css seinen Platz hat (s. Ordner-Struktur). Bitte habt Verständnis. Ich habe mir nicht die Mühe gemacht, den Source-Code zu optimieren. Manches wird man sicher besser machen können.
* { margin:0; padding:0; } .clear-fix { clear:both; border:0; } label { font-size: 0.83em; -webkit-margin-before: 1.67em; -webkit-margin-after: 1.67em; -webkit-margin-start: 0px; -webkit-margin-end: 0px; font-weight: bold; } h5 { margin-bottom:0.2em; } .important { font-style: italic; } .form-entity { padding: 0.5em 0; display:none; } body { background: #ABCCDE; } header, main, footer { min-width: 855px; } header .container, main .container, footer .container { max-width: 855px; margin: auto; } header { background: white; overflow:auto; padding: 1em 0 } header h1 { font-family: 'Denk One', sans-serif; float:left; width: 490px; padding-left:16px; } header div.logo-header-main { width: 64px; float:left; } header div.logo-text-main { float:left; } header nav { float:right; width: 285px; } header nav a, header nav a:visited { font-family: 'arial'; margin-right:0.5em; color: #000000; text-decoration: none } header nav a:hover { color: red; } main { overflow: auto; overflow-x: hidden; padding: 1em 0; min-height: 300px; } main #form-action-container { width: 285px; float:left; } main aside { float:right; width: 285px; } main aside h5 { margin-left: 185px; } main textarea, #cssConsole { width: 570px; height: 220px; margin-left: -285px; font-family: "courier"; font-size: 0.9em; background-color: #000000; color:#ffffff; padding:0.3em; text-align:left; white-space: normal; /* otherwise first line is centered */ position:relative; overflow:hidden; } main textarea { display: none; } #input { width: 460px; display:inline-block; position:relative; float:left; } .bottom { position:absolute; bottom:5px; width: 100%; } .label { width: 30px; position: relative; left: 15px; top:5px; color: #fefefe; float:left; } .console { position:absolute; bottom:32px; overflow:hidden; } .white{ color: #fefefe; } .blue{ color: #81bbd5; } .line { margin-left: 15px; margin-bottom:2px; } .margin { margin-left: 40px; margin-bottom:2px; } footer { background: #2B67A4; color: white; overflow:auto; padding:1em 0; } footer .contact { float:left; width: 250px; } footer .copyright { float:left; } footer .sitemap { float:right; width: 285px; } footer .contact p { text-transform:uppercase; font-size: 0.8em; } footer .sitemap ul { list-style-type: none; } footer a, footer a:visited { font-family: 'arial'; font-size: 0.8em; color: white; text-decoration: none } footer a:hover{ color:red; }
Damit sind alle Vorbereitungen abgeschlossen und wir können zum Kern der Applikation kommen, den beiden Skripten für Server und Client.
Das Server-Skript server.js
Das folgende Server-Script server.js (s. auch Projektstruktur) enthält noch ein paar Routes aus den vorhergehenden Tests. Ich habe sie einfach mal dringelassen, damit die Struktur des Programms besser erkennbar bleibt.
Man kann mit diesen Test-Routes auch erste Funktionstest durchführen, so dass sie tatsächlich noch einen ganz praktischen Zweck erfüllen.
var express = require("express"), http = require("http"), child_process = require("child_process"), app = express(), aup = require("./ansi_up/ansi_up.js"); // Folgende Zeile ausklammern, dann gilt für / route ganz unten. app.use(express.static(__dirname + "/client")); // Add headers. Another possibility is to start chrome with '--disable-web-security' // http://stackoverflow.com/questions/18310394/no-access-control-allow-origin-node-apache-port-issue // Otherwise you get an error on putting data from client to server like: ".... Origin 'http://127.0.0.1:3000' is therefore not allowed access. ...." app.use(function (req, res, next) { // Website you wish to allow to connect res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:3000'); // not 'localhost' !!!!! // Request methods you wish to allow res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); // Request headers you wish to allow res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); // Set to true if you need the website to include cookies in the requests sent // to the API (e.g. in case you use sessions) res.setHeader('Access-Control-Allow-Credentials', true); // Pass to next layer of middleware next(); }); var cpr = null; // socket.io var io = require('socket.io').listen(8000); io.sockets.on('connection', function(socket) { socket.on('console-input', function (data) { if (data.from == 'textarea') { // textarea process.stdout.write(String.fromCharCode(data.character)); } else { process.stdout.write(data.character); } if (cpr !== null) { if (data.from == 'textarea') { // textarea cpr.stdin.write(String.fromCharCode(data.character)); } else { cpr.stdin.write(data.character); } } }); socket.on('client-message', function (data) { process.stdout.write(data.message); }); console.log('client connected!'); }); // Express http.createServer(app).listen(3000); // Routes app.get("/", function(req, res) { res.send("This is the root route!"); }); app.get("/hello", function(req, res) { res.send("Hello World"); }); app.get("/goodby", function(req, res) { res.send("Goodby World"); }); app.post("/test", function(req, res) { console.log('***** node.js: test *****'); cwd = 'C:\\xampp\\htdocs\\CAWD\\customers\\hauertmann.com\\projects\\Grunt'; doCommand(cwd, ['test', '--target', req.query.target]); // send back a simple object res.json({message: "***** Server response: test - posted to the server! *****"}); io.sockets.emit('console-output', { line: '***** test: emitted from server to client *****\r\n', time: timestamp() }); }); console.log('Start application in your Browser with localhost:3000'); function doCommand(cwd, task) { var grunt = ['c:\\Users\\admin\\AppData\\Roaming\\npm\\node_modules\\grunt-cli\\bin\\grunt']; cpr = child_process.spawn( 'C:\\Program Files\\nodejs\\node.exe', // command grunt.concat(task), // arguments {'cwd': cwd} // options ); cpr.stdout.on('data', function (data) { console.log('stdout: ' + data); var console_output = sanitizeConsoleOutput(data); io.sockets.emit('console-output', { line: '' + console_output, time: timestamp() }); }); // Achtung: Auch hierüber können Abfragen erfolgen, z.B. remote bei: rm -rI // Hier erfolgt die Abfrage, ob tatsächlich gelöscht werden soll, über stderr! // Deshalb muss auch dieser Output auf die cssConsole geleitet werden, da // sonst die Frage nicht sichtbar wird. cpr.stderr.on('data', function (data) { console.log('stderr: ' + data); var console_output = sanitizeConsoleOutput(data); io.sockets.emit('console-output', { line: 'system: ' + console_output, // I name it 'system', not stderr! time: timestamp() }); }); cpr.on('close', function (code) { console.log('child process exited with code ' + code); cpr = null; }); } // ------------------------------------------------------------------------ function sanitizeConsoleOutput(data) { // ArrayBuffer to String var console_output = '' + ab2str(data); // replace color-codes // for (var ix=1; ix<50; ix++) { // console_output = replace(console_output, '\x1b['+ix+'m', ''); // } console_output = aup.ansi_to_html(console_output); // whitelist of allowed characters var pattern = /[^a-zA-Z0-9öäüßÖÄÜ!@#$%^&*() _+\-=\[\]{};':"\\|,.<>\/?\n\r]/g; var console_output = console_output.replace(pattern, ''); return console_output; } function ab2str(buf) { return String.fromCharCode.apply(null, new Uint16Array(buf)); } function str2ab(str) { var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char var bufView = new Uint16Array(buf); var strLen=str.length; for (var i=0; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } // replace all occurrences of one ANSI escape sequence with another. function replace(str, sequence, replacement) { // Validate input sequence and extract color number. var code = sequence.match(/^\x1b\[(\d+)m$/); if (!code) { // Handle invalid escape sequence. console.log('Error! Invalid escape sequence'); return str; } // make it a regexp and replace all occurrences with the start color code return str.replace(new RegExp('\\x1b\\[' + code[1] + 'm', 'g'), replacement); } function timestamp() { var now = new Date(); var date = [ now.getFullYear(), now.getMonth() + 1, now.getDate()]; var time = [ now.getHours(), now.getMinutes(), now.getSeconds() ]; for ( var i = 1; i < 3; i++ ) { if ( time[i] < 10 ) { time[i] = "0" + time[i]; } } for ( var i = 1; i < 3; i++ ) { if ( date[i] < 10 ) { date[i] = "0" + date[i]; } } return date.join("") + "-" + time.join(""); }
Wer den Beitrag zum schnellen Einstieg in Node.js gelesen hat, wird die Struktur des Skripts verstehen. Es enthält an einigen Stellen Erläuterungen, so dass ich mir die Diskussion dieser Punkte hier erspare.
Gegenüber den einfachen Beispielen im vorhergehenden Artikel ist hier deshalb nur noch auf den Abschnitt socket.io einzugehen.
In den einführenden Beispielen wurde ja nur Inhalt ausgeliefert. Es wurden keine Daten vom Client entgegen genommen.
In unserer GUI-Anwendung reicht dies nun nicht mehr. Hier müssen zum einen die Anforderung der Auswahl- und Eingabe-Elemente der GUI als auch der Input von der cssConsole verarbeitet werden.
Hierfür benötigt man das Module socket.io, welches hier für die Verarbeitung des Input-Streams der cssConsole eingesetzt wird.
Die Auswertung der übrigen GUI-Elemente geschieht über Ajax, wobei hier die Möglichkeiten des Express-Moduls genutzt wird, welches zur Auswertung entsprechend übertragener Daten die Funktion app.post(...) zur Verfügung stellt (eine Funktion app.get(...) gib es natürlich auch, aber die kannten wir ja schon aus unseren Beispielen).
Genauer anschauen sollte man sich die Implementierung von app.post("/test", ....), welche unser Grunt-Skript startet.
Das Grunt-Test-Skript Gruntfile.js
Unser Demo-Grunt-Skript kann man in einem beliebigen Ordner anlegen außerhalb unseres GUI-Projekts. Dies entspricht auch der Realität. Man wird mit unserem Skript verschiedene Grunt-Skripte starten können, die in verschiedenen beliebigen Projekt-Ordnern liegen dürfen. Wie man ein Grunt-Projekt anlegt, das kennen wir aus unserer Grunt-Beitrags-Reihe, weshalb hier nur das Gruntfile.js dargelegt wird. Als Hinweis noch die Anmerkung, dass die Grunt-Module grunt-shell und grunt-prompt für die Ausführung benötigt werden.
'use strict'; module.exports = function(grunt) { grunt.initConfig({ pkg : grunt.file.readJSON('package.json'), prompt: { test: { options: { questions: [{ config: 'mymessage', type: 'input', message: '[Enter] to continue ...' }] } }, commit: { options: { questions: [{ config: 'Bestätigung', type: 'input', message: 'Möchten Sie wirklich fortfahren ? [j,N]: ' }] } }, shell : { //----------- Test --------- "test" : { command : [ 'echo ***** test, test, test *****', ].join('&&') }, } }); // grunt.registerTask('test', ['shell:test']); grunt.registerTask('test_answer', 'Auswerten der Prompt-Eingabe', function() { var answer = grunt.config("mymessage"); grunt.log.write('Answer: ' + answer + '\r\n'); grunt.log.write('Abfrage beendet!').ok(); }); grunt.registerTask('test', 'Testen mit Prompt-Eingabe', function(){ grunt.task.run('prompt:test'); grunt.task.run('test_answer'); grunt.task.run('shell:test'); }); grunt.registerTask('commit_answer', 'Auswerten der Prompt-Eingabe', function() { var answer = grunt.config("Bestätigung"); grunt.log.write('Answer: ' + answer + '\r\n'); if (answer !== 'j') { grunt.fail.warn('Sie haben mit "Nein" geantwortet!'); grunt.log.write('Vorgang abgebrochen!').error(); } else { grunt.log.write('Sie müssen wissen, was Sie tun.').ok(); } }); };
Das Skript erzeugt im wesentlichen ein paar Ausgaben auf der Windows-Console und enthält vor allen Dingen einen Abfrage-Prompt, der eine Eingabe erwartet, die später dann vom Server-Skript ausgewertet werden wird und dann dem Grunt-Prozess für die weitere Abarbeitung zur Verfügung gestellt wird, so dass der Gruntprozess abhängig von der Eingabe gesteuert wird.
Das Client-Skript app.js
Schauen wir uns nun das Client-Skript app.js im Ordner client/assets/js an. Dieses Skript
- steuert die GUI-Elemente, die abhängig von der jeweiligen Auswahl angezeigt werden,
- führt Ajax-Requests an den Server aus abhängig von den GUI-Events (z.B. onSelect) und
- sendet Daten der Console über die Socket-Verbindung (socket.io) zum Server, so dass die Eingaben per Console dort in Echtzeit verarbeitet werden können.
var main = function() { 'use strict'; var socket = io.connect('http://localhost:8000', {'connect timeout': 1000}); $(document).ready(function() { $('#input').find('input').focus(); }); // $('#cssConsole').cssConsole(); $('#input').cssConsole({ inputName:'console', charLimit: 40, onEnter: function(){ addLine("> "+$('#input').find('input').val()); // execCommand($('#input').find('input').val()); socket.emit('console-input', { character: $('#input').find('input').val() + '\r\n', from: 'cssConsole' }); $('#input').cssConsole('reset'); $('#input').find('input').focus(); } }); var lineLimit = 28; $('#cssConsole').on('click', function() { $('#input').find('input').focus(); }); function addLine(input, style, color) { if($('.console div').length==lineLimit) { $('.console div').eq(0).remove(); } style = typeof style !== 'undefined' ? style : 'line'; color = typeof color !== 'undefined' ? color : 'white'; $('.console').append('<div class="'+style+' '+color+'">'+input+'</div>'); } function execCommand(command){ if ( commands[command] ) { return commands[command](); } else { addLine("Command '" + command + "' was not found."); } } var commands = { help: function (){ addLine("Available command list:"); addLine("dir", 'margin'); addLine("help", 'margin'); addLine("ps", 'margin'); }, dir: function(){ addLine("."); addLine(".."); addLine("Applications", 'margin', 'blue'); addLine("Documents", 'margin', 'blue'); addLine("Downloads", 'margin', 'blue'); addLine("Movies", 'margin', 'blue'); addLine("Music", 'margin', 'blue'); addLine("System", 'margin', 'blue'); }, ps: function() { addLine("Running processes:"); addLine("name: browser pid:8876", 'margin'); addLine("name: movie player pid:3213", 'margin'); addLine("name: system pid:0012", 'margin'); } } var setGuiByAction = function(action) { switch(action) { case 'test': $("#select-target").parent().show(); $("#select-source").prev().hide(); $("#select-source").hide(); $("#button-submit").parent().show(); enableSelectLocal(true); $("#select-db-part").parent().hide(); break; case 'upgrade': $("#select-target").parent().show(); $("#select-source").prev().hide(); $("#select-source").hide(); $("#button-submit").parent().show(); enableSelectLocal(true); $("#select-db-part").parent().hide(); break; case 'deploy': $("#select-target").parent().show(); $("#select-source").prev().hide(); $("#select-source").hide(); $("#button-submit").parent().show(); enableSelectLocal(false); $("#select-db-part").parent().hide(); break; case 'deploy-new': $("#select-target").parent().show(); $("#select-source").prev().hide(); $("#select-source").hide(); $("#button-submit").parent().show(); enableSelectLocal(false); $("#select-db-part").parent().hide(); break; case 'backup-db': $("#select-target").parent().show(); $("#select-source").prev().hide(); $("#select-source").hide(); $("#button-submit").parent().show(); enableSelectLocal(true); $("#select-db-part").parent().hide(); break; case 'copy-db': $("#select-target").parent().show(); $("#select-source").prev().show(); $("#select-source").show(); $("#button-submit").parent().show(); enableSelectLocal(true); $("#select-db-part").parent().show(); break; case 'backup-sites': $("#select-target").parent().show(); $("#select-source").prev().hide(); $("#select-source").hide(); $("#button-submit").parent().show(); enableSelectLocal(true); $("#select-db-part").parent().hide(); break; case 'maintenance-on': $("#select-target").parent().show(); $("#select-source").prev().hide(); $("#select-source").hide(); $("#button-submit").parent().show(); enableSelectLocal(false); $("#select-db-part").parent().hide(); break; case 'maintenance-off': $("#select-target").parent().show(); $("#select-source").prev().hide(); $("#select-source").hide(); $("#button-submit").parent().show(); enableSelectLocal(false); $("#select-db-part").parent().hide(); break; case 'update-cc': $("#select-target").parent().show(); $("#select-source").prev().hide(); $("#select-source").hide(); $("#button-submit").parent().show(); enableSelectLocal(false); $("#select-db-part").parent().hide(); break; default: $("#select-project").parent().show(); $("#select-action").parent().show(); $("#select-db-part").parent().hide(); $("#select-source").parent().hide(); $("#select-target").parent().hide(); $("#button-submit").parent().hide(); enableSelectLocal(false); } } $('#button-submit').on('click', function(event) { if ($('#select-action').val() == 'test') { // window.location.href = "http://localhost:3000/upgrade?target=" + $("#select-target").val(); // get: /* $.get("http://localhost:3000/test?target=" + $("#select-target").val(), function(req, res) { socket.emit('client-message', { message: '***** client: test ***** ***** ***** ***** ***** *****' }); }); */ // post: $.post("http://localhost:3000/test?target=" + $("#select-target").val(), {}, function(response) { console.log('We posted and the server responded!'); console.log(response); socket.emit('client-message', { message: '***** client: test ***** ***** ***** ***** ***** ***** -> ' + response.message }); }); } if ($('#select-action').val() == 'upgrade') { $.post("http://localhost:3000/upgrade?target=" + $("#select-target").val(), {}, function(response) { socket.emit('client-message', { message: '***** client: upgrade ***** ***** ***** ***** ***** *****' }); }); } if ($('#select-action').val() == 'deploy') { $.post("http://localhost:3000/deploy?target=" + $("#select-target").val(), {}, function(response) { socket.emit('client-message', { message: '***** client: deploy ***** ***** ***** ***** ***** *****' }); }); } if ($('#select-action').val() == 'deploy-new') { $.post("http://localhost:3000/deploy-new?target=" + $("#select-target").val(), {}, function(response) { socket.emit('client-message', { message: '***** client: deploy-new ***** ***** ***** ***** ***** *****' }); }); } if ($('#select-action').val() == 'backup-db') { $.post("http://localhost:3000/backup-db?target=" + $("#select-target").val(), {}, function(response) { socket.emit('client-message', { message: '***** client: db-backup ***** ***** ***** ***** ***** *****' }); }); } if ($('#select-action').val() == 'copy-db') { $.post("http://localhost:3000/copy-db?target=" + $("#select-target").val() +'&' + "source=" + $("#select-source").val()+'&' + "part=" + $("#select-db-part").val(), {}, function(response) { socket.emit('client-message', { message: '***** client: db-copy ***** ***** ***** ***** ***** *****' }); }); } if ($('#select-action').val() == 'backup-sites') { $.post("http://localhost:3000/backup-sites?target=" + $("#select-target").val(), {}, function(response) { socket.emit('client-message', { message: '***** client: site-backup ***** ***** ***** ***** ***** *****' }); }); } if ($('#select-action').val() == 'maintenance-on') { $.post("http://localhost:3000/maintenance-on?target=" + $("#select-target").val(), {}, function(response) { console.log('We posted and the server responded!'); console.log(response); socket.emit('client-message', { message: '***** client: maintenance-on ***** ***** ***** ***** ***** *****' }); }); } if ($('#select-action').val() == 'maintenance-off') { $.post("http://localhost:3000/maintenance-off?target=" + $("#select-target").val(), {}, function(response) { console.log('We posted and the server responded!'); console.log(response); socket.emit('client-message', { message: '***** client: maintenance-off ***** ***** ***** ***** ***** *****' }); }); } if ($('#select-action').val() == 'update-cc') { $.post("http://localhost:3000/update-cc?target=" + $("#select-target").val(), {}, function(response) { console.log('We posted and the server responded!'); console.log(response); socket.emit('client-message', { message: '***** client: update-cc ***** ***** ***** ***** ***** *****' }); }); } // $('#textarea-console').focus(); $('#input').find('input').focus(); // later needed again /* var console = $('#textarea-console'); console.append('Hello World '+Math.random()+'\n'); if(console.length) console.scrollTop(console[0].scrollHeight - console.height());*/ }); $('#select-project').prop('disabled', true); $("#select-action").val('none'); $("#select-project").parent().show(); $("#select-action").parent().show(); enableSelectLocal(false); $("#select-db-part").parent().hide(); $("#textarea-console").parent().show(); $('#select-action').change(function() { setGuiByAction($( "#select-action" ).val()); }); $('#textarea-console').text('Hi!\n'); function enableSelectLocal(enable) { if (enable) { $("select option[value='local']").removeAttr('disabled') ; } else { $("select option[value='local']").attr('disabled','disabled') ; $("select option[value='local']").attr('disabled','disabled') ; $("#select-source").val("test"); $("#select-target").val("test"); } } socket.on('console-output', function (data) { // This doesn't work. Don't know why! On a second event from server nothing will be displayed. // $('#textarea-console').append(data.time + ': ' + data.line+'\n'); // This worked! I found it here: http://stackoverflow.com/questions/5203428/inserting-text-after-cursor-position-in-text-are%D0%B0/15945971#15945971 var position = $('#textarea-console').getCursorPosition(); var content = $('#textarea-console').val(); var newContent = content.substr(0, position) + data.line + content.substr(position); $('#textarea-console').val(newContent); // not used here but interesting: // to insert text at the position: // var content = $('#textarea-console').val(); // var newContent = content.substr(0, position) + "text to insert" + content.substr(position); // $('#textarea-console').val(newContent); newContent = replaceAll(newContent, '\n', '</br>'); $('.console').html(newContent); // scroll to the end of text if ($('#textarea-console').length) $('#textarea-console').scrollTop($('#textarea-console')[0].scrollHeight - $('#textarea-console').height()); }); $('#textarea-console').keypress(function( event ) { //if ( event.which == 13 ) { // event.preventDefault(); //} socket.emit('console-input', { character: event.which, from: 'textarea' }); }); } $(document).ready(main); (function ($, undefined) { // textarea $.fn.getCursorPosition = function () { var el = $(this).get(0); var pos = 0; if ('selectionStart' in el) { pos = el.selectionStart; } else if ('selection' in document) { el.focus(); var Sel = document.selection.createRange(); var SelLength = document.selection.createRange().text.length; Sel.moveStart('character', -el.value.length); pos = Sel.text.length - SelLength; } return pos; } })(jQuery); // needed for '_replaceAll' function escapeRegExp(string) { return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); } function replaceAll(string, find, replace) { return string.replace(new RegExp(escapeRegExp(find), 'g'), replace); }
Das Script dürfte weitgehend selbsterklärend sein. Erwähnenswert ist noch, dass hier das Modul ansi_up zum Einsatz kommt, welches Steuersequenzen z.B. zur Anzeige von Farben in für die cssConsole verständliche Html/CSS-Syntax umwandelt.
Außerdem werden die Eingaben aus der Console mittels einer Reg-Expression, die als eine Art Whitelist für Character fungiert, gefiltert.
Das Ergebnis ist leider nicht ganz clean, weil auch Steuersequenzen vorkommen, die sich z.B. auf die Position des Cursors in der Console beziehen. Damit ließen sich grundsätzlich Progress-Bars und Splashs für Prozess-läuft-Anzeigen realsieren.
Ich werde diese Fälle vielleicht in späteren Versionen berücksichtigen. Jeder Vorschlag oder Hinweis auf Implementierungen für dieses Problem sind natürlich willkommen.
Das Ergebnis
Wenn alles funktioniert, sollte das Ergebnis der Ausführung der Grunt-Task grunt test etwa so aussehen (vgl. Abb. 2):
Abb. 2: Ausführung einer Test-Grunt-Task mit der GUI
Man erkennt schön, dass mehrfarbige Ausgaben möglich sind wegen des Einsatzes von Ansi Up, dass aber noch etwas Arbeit in die Verarbeitung von Steuersequenzen hineingesteckt werden muss, da offensichtlich z.B. das Steuerzeichen ESC [ ? 25 h = Set Cursor ON nicht prozessiert wird. Das ist kein Fehler, es wurde einfach noch nicht implementiert.
Trotz solch kleiner Hakeleien ist die Ausführung von Grunt-Tasks auf diese Weise schon sehr bequem, wie ich aus meiner Erfahrung mit meiner großen Lösungen bestätigen kann.
Fazit
Das vorgestellt Beispiel zeigt eigentlich maximal die Möglichkeiten, die Node.js für viele denkbare Einsatzfälle im persönlichen und professionellen Bereich bietet. Es zeigt, wie man
- Http-Server einrichtet
- Ajax-Anforderungen verabeitet und
- per Socket-Verbindungen Eingabe- oder sonstige Input-Streams verarbeitet.
Mit den gleichen Problemlösungen, wie hier vorgestellt, könnte man auch einen Chat realisieren, das typische Beispiel für Node.js.
Ich finde, meine Consolen-Applikation hat da mehr Potential im Hinblick auf jede Menge Ideen für mögliche Einsatzbereiche, denn Chat-Programme gibt es schließlich, wie Sand am Meer.