The days before Christmas were busy as usual: It’s not just that everyone is hunting gifts for family and friends. There’s also the annual German Youth Team Championship of Chess beginning on 25th December. Together with some friends I’m trying to broadcast this big event with more than 500 young participants to their families left at home, waiting for results.
In the last years, we used a simple mechanism to provide near to live results: The arbiters got a special email adress where they could send their tournament files to. On the server side there was a small node.js mail server (as mentioned in “Einfacher SMTP-Server zur Aufgabenverarbeitung” [german]) that took the proprietary file format, converted it and imported the results of already finished games. Although being a huge progress to the past where the results were imported once all games has been finished, this approach needed an arbiter constantly sending mails around.
Therefore I wanted to try another way: A program that keeps an eye on the tournament file and uploads it once it was changed and automatically triggers the import of new game results. Having just some days for its development it was necessary to stay with the same technology stack we used before for the mail server and tournament file converter: node.js.
As the tournament arbiters aren’t all familiar with command line tools, a graphical user interface was necessary. Strongloop published a blog post about node-webkit, which allows writing native apps in HTML and Javascript, some days before. This blog post is a good entry to the topic. Nettuts+ wrote a nice introduction recently too. Different from their approach I used the plugin for Grunt grunt-node-webkit-builder, which takes on the whole building process. Here’s my project’s setup:
/ ├── dist ├── Gruntfile.js ├── package.json └── src ├── index.html ├── package.json ├── js │ └── index.js └── css └── style.css
By using the grunt-node-webkit-builder
it is necessary to keep the source of the building tool (all in the root directory) separate from the source code of the node-webkit program. Otherwise it may happen that the building tools (Grunt, you know?) get bundled in the node-webkit program as well which leads to high file sizes and slow execution times.
So it’s clear we specify in /package.json
only the dependencies that are necessary for the building process:
{ "name": "do-my-build", "version": "0.0.1", "description": "Using Grunt to build my little program", "author": "Falco Nogatz <fnogatz@gmail.com>", "private": true, "dependencies": { "grunt": "~0.4.2", "grunt-node-webkit-builder": "~0.1.14" } }
We also have to create the Gruntfile.js
:
module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('src/package.json'), nodewebkit: { options: { build_dir: './dist', // specifiy what to build mac: false, win: true, linux32: false, linux64: true }, src: './src/**/*' }, }); grunt.loadNpmTasks('grunt-node-webkit-builder'); grunt.registerTask('default', ['nodewebkit']); };
The real node-webkit
program can be written now in the /src
directory. As also mentioned in the tutorials linked above, the /src/package.json
should be filled with some node-webkit
related fields:
{ "name": "my-program", ... "main": "index.html", "window": { "toolbar": false, "width": 800, "height": 600 } }
To build the node-webkit
program for the architectures specified in /package.json
you simply have to call the command:
grunt
This downloads the up-to-date binaries necessary for the specified architectures and builds an executable program. The result for Windows is simply a .exe
file, for Linux an executable file. It contains all needed to run the program, so the user neither has to install node.js nor Chrome. The builds are located in /dist/releases
.
By using this setup it was possible to automate the building process and develop the application within some days. The node-webkit
runtime extends some native browser properties, for example it is possible to get the full path of a file selected by an <input type="file">
. With that it was possible to create a graphical user interface to select tournament files and watch for their changes, which would trigger the update process.
set keymap vi
set editing-mode vi
The motivation for this comes from the lecture Mobile Human Computer Interaction by Enrico Rukzio where the exercises demand such kind of presentation. But I will not go into much detail about this, the more important questions is: Does the current presentation-programs feature this technique? And the answer is: Yes, but often presentations are held with PDFs which do not feature it.
So I decided to write a little java-application that extends every presentation-program with this feature. I used java because it is platform independent and I wanted to be compatible with most programs and most OS. Also java has a very (very) cool class called
java.awt.Robot
which allows you to send keystrokes system-wide! Yes, pretty cool; and it works for Windows, Linux and Mac.
This also explains the way I implemented it. Just sending Page-Down keystrokes to the system, when the presentation is running. This will click the next slide in a given amount of time automatically. Page-Down because most hardwarepresenters also use this keystroke to fulfill their purpose.
This post is about how I program little applications and should give everyone a little look how I write code. One important thing I learned at the University is the importance of requirements and software engineering. In short: Think about what you do, the program must exist in your head (or at least on papers and diagrams) before a single line of code is written. Even is such small programs like this one.
Let’s do some requirements engineering:
The first thing we need to know is, what should your program do, and more important, what should it not do!
So, we need a program that should automatically switch slides on a presentation in a given amount of time. The program should work with most OS and most presentation-programs. Controlling the program should be possible via a Trayicon and Formdialog. Every controlling-element should indicate if the timer is started or stopped. Before the program should switch slides, the user must have some time to prepare for his presentation. Therefore a delay should be implemented that starts the input-automation after a fix amount of time. The program should always give responses to the internal state, like: How much time is left before the presentation starts and how long will the current slide last until a switch. Also it should be possible to pause the program while in presentation. If the presentation has ended, you should be able to start over. It should be possible to set parameters for: Delay-before-presentation, Time-for-each-slide, Amnout-of-slides-in-total.
The requirements could be more precise, but that should be enough.
The next thing is to think about how to implement it. Usually you draw nice diagrams like Class-diagrams and state charts.
As we can see in the diagrams above, the Presenter is our Model (the M in MVC), Tray and InfoDisplay are Views but also Controllers. If we switch the state of our Model via the Controller, the Views (which implement the PresenterStateListener) are updated automatically. As timer I used a Ticker-Class which just calls a callback in the presenter ( tick() ) every second. The ticker can be in play or in paused state. PresenterController implements the Play/Pause state machine and the Listeners, Presenter implements the delay/next-slide state machine.
But enough about intention, requirements and design, let’s go see some code!
public class Main { public static void main(String[] args) { int delayFirst = 10; int slideCount = 15; int slideSeconds = 20; if (args.length == 3){ delayFirst = Integer.parseInt(args[0]); slideCount = Integer.parseInt(args[1]); slideSeconds = Integer.parseInt(args[2]); } Presenter p = new Presenter(delayFirst,slideCount,slideSeconds); InfoDisplay d = new InfoDisplay(p); d.setVisible(true); Tray t = new Tray(p,d); p.addPresenterStateListener(t); p.addPresenterStateListener(d); } }
This part should be obvious. We control alternative parameters via command-line args, then delegate everything to the Presenter. After that, we create the Views and connect the Listeners.
Let’s have a look into the Tray-Class, which I developed first, because it’s often easier to begin with a controlling element like a GUI. This allows you to implement the use-cases defined in the requirements-part and also allows you to find errors and misunderstandings between Customer and Contractor in a very early state of development. If all interactions are implemented, you can run usability-tests with real users. But personally I think beginning with controlling elements allows you develop an early logic which can be very good integrated into the final program.
Back to the Tray-Class:
/** * * Spawns a trayicon to control the presentation (play/pause) * */ public class Tray implements PresenterStateListener, ActionListener { //this is a nice way to access ressources in the jar-file. But this also works in the normal filesystem. private final Image playImage = Toolkit.getDefaultToolkit().getImage( Tray.class.getResource("/resources/play.png")); private final Image pauseImage = Toolkit.getDefaultToolkit().getImage( Tray.class.getResource("/resources/pause.png")); private final String TOOLTIP_PLAYING = "pechakucha - playing"; private final String TOOLTIP_STOPPED = "pechakucha - stopped"; private TrayIcon trayIcon; private final PopupMenu menu = new PopupMenu(); private final MenuItem exit_menu_item = new MenuItem("exit"); private final MenuItem toggle_gui = new MenuItem("toggle gui"); private final Presenter p; private final JFrame gui; private boolean disableTrayNotifications = true; public boolean isDisableTrayNotifications() { return disableTrayNotifications; } public void setDisableTrayNotifications(boolean disableTrayNotifications) { this.disableTrayNotifications = disableTrayNotifications; } public Tray(Presenter p, JFrame f) { gui = f; trayIcon = new TrayIcon(playImage, TOOLTIP_STOPPED); this.p = p; trayIcon.setImageAutoSize(true); trayIcon.setPopupMenu(menu); menu.add(exit_menu_item); exit_menu_item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.exit(0); } }); toggle_gui.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (gui.isVisible()) { gui.setVisible(false); } else { gui.setVisible(true); } } }); menu.add(toggle_gui); if (SystemTray.isSupported()) { SystemTray tray = SystemTray.getSystemTray(); try { tray.add(trayIcon); } catch (AWTException e) { e.printStackTrace(); } } trayIcon.addActionListener(this); } public void setMessage(String msg) { if (disableTrayNotifications) return; trayIcon.displayMessage("pechacucha", msg.replaceAll("<br>", "\n"), MessageType.INFO); } @Override public void presenterPlay() { trayIcon.setImage(pauseImage); trayIcon.setToolTip(TOOLTIP_PLAYING); } @Override public void presenterPause() { trayIcon.setImage(playImage); trayIcon.setToolTip(TOOLTIP_STOPPED); } @Override public void actionPerformed(ActionEvent e) { this.p.switchState(); } @Override public void presenterInfoUpdate(String msg) { setMessage(msg); } }
The Trayicon should be very straight forward, there should be no problems to understand the code here.
The InfoDisplay class is also a very straight forward Swing JFrame:
/** * * Displays a play/pause button for the presentation, * a timer that shows when the presentation starts, when the next slide will be shown * and how many slides are left. * */ public class InfoDisplay extends JFrame implements PresenterStateListener, ActionListener { private static final long serialVersionUID = 1L; private JLabel body; private final JButton playpause; private final Presenter p; public InfoDisplay(Presenter p) { setSize(800, 300); playpause = new JButton("play"); getContentPane().add(playpause, BorderLayout.SOUTH); setDefaultCloseOperation(EXIT_ON_CLOSE); playpause.addActionListener(this); this.p = p; body = new JLabel(); body.setHorizontalAlignment(SwingConstants.CENTER); setMessage("press play to start"); getContentPane().add(body, BorderLayout.CENTER); setTitle("pechacucha"); setLocationRelativeTo(null); setAlwaysOnTop(true); } public void setMessage(String msg) { //interesting things could be the String.format() method, which works like c's printf. Also nice is the usage of html and css to style the Label. this.body .setText(String .format("<span style="font-size: 40pt; font-weight: bold; text-align: center;">%s</span>", msg)); } @Override public void presenterInfoUpdate(String msg) { setMessage(msg); } @Override public void presenterPlay() { this.playpause.setText("pause"); } @Override public void presenterPause() { this.playpause.setText("play"); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == playpause) { this.p.switchState(); } } }
Now we have seen the View and Controllers combined as Trayicon/JFrame and PresenterStateListener
Lets look at some logic:
To make things easier, we will only look at the interesting parts of the logic and omit some boring getters, setters, variable declarations,…
As mentioned earlier the PresenterController implements the play/pause state machine:
protected enum State { PLAYING, STOPPED; } protected State state; protected abstract void stateChangedStopped(); protected abstract void stateChangedStarted(); /** * switches the state to play/pause; depends on the previous state */ public void switchState() { if (state == State.PLAYING) { state = State.STOPPED; for (PresenterStateListener l : listeners) { l.presenterPause(); } stateChangedStopped(); } else { state = State.PLAYING; for (PresenterStateListener l : listeners) { l.presenterPlay(); } stateChangedStarted(); } }
On top of that, I created the Presenter class which uses a Ticker to time events. Every second the ticker calls the tick-method of the presenter. The presenter then decides what to do: Delay the presentation, count down the slide-timer or use the robot to send a “next-slide”-keystroke to the operating system.
/** * * The main control class for presentation. * Uses the Ticker to periodically perform an action (tick()) * * Implements the next-slide, presentation starts, ends state machine. * */ public class Presenter extends PresenterController { private final int delayFirstSeconds; private int delayFirstSecondsPassed; private final int numSlides; private int slideCount = 0; private boolean firstPlay = true; private final int secondsForSlide; private int secondsForSlidePassed; private Robot robot; private final Ticker ticker; public Presenter(int delayFirstSeconds, int numSlides, int secondsForSlide) { super(); resetState(); this.delayFirstSeconds = delayFirstSeconds; this.numSlides = numSlides; this.secondsForSlide = secondsForSlide; ticker = new Ticker(this); try { robot = new Robot(); } catch (AWTException e) { e.printStackTrace(); } } /** * resets the state of the presenter */ private void resetState() { delayFirstSecondsPassed = 0; secondsForSlidePassed = 0; slideCount = 0; firstPlay = true; } /** * performs a systemwide "next-slide" keystroke (pgdown) */ public void nextSlide() { robot.keyPress(KeyEvent.VK_PAGE_DOWN); robot.keyRelease(KeyEvent.VK_PAGE_DOWN); } @Override protected void stateChangedStopped() { ticker.pause(); } @Override protected void stateChangedStarted() { ticker.play(); if (firstPlay) { firstPlay = false; } } /** * callback for the ticker */ public void tick() { delayFirstSecondsPassed++; if (delayFirstSecondsPassed >= delayFirstSeconds) { secondsForSlidePassed++; // delay over, start if (firstPlay) { firstPlay = false; } if (secondsForSlidePassed >= secondsForSlide) { // next slide slideCount++; secondsForSlidePassed = 0; if (numSlides - slideCount // presentation over switchState(); msgPresenterStateListeners("presentation finished"); resetState(); return; } nextSlide(); } msgPresenterStateListeners("Seconds until next slide: " + (secondsForSlide - secondsForSlidePassed) + " Slides left: " + (numSlides - slideCount - 1)); } else { // delay phase msgPresenterStateListeners("Seconds until start: " + (delayFirstSeconds - delayFirstSecondsPassed)); } } }
Then there is the Ticker class. The idea of the ticker is, that he has three states: PLAY, PAUSE, STOP. However STOP is never active because it would prevent the ticker from starting over. Once the run-method has ended, a Thread will never be alive again..
/** * * Performs an action (callbacks the Presenter's tick method) * every second. * * The ticker can be in three states: PLAY, PAUSE, STOP * On Play, every seconds the callback will be called, * on Pause, the ticker-thread will be on wait. * On Stop, the Ticker can never be in play or paused state again. * */ public class Ticker implements Runnable { //The volatile keyword is not as easy explained as you might think, //in short, use it to indicate that multiple threads will access it. //Also note that the definition of volatile tightened up in java5. private volatile boolean stopped = false; private Thread ownThread; private long lastSecond = 0; private int every; //every.. second -> every=1 private enum TickerState { PLAY, PAUSE, STOP; } private volatile TickerState state; private Presenter p; public Ticker(Presenter p) { this(p, 1); } public Ticker(Presenter p, int every) { this.p = p; this.every = every; state = TickerState.PAUSE; ownThread = new Thread(this); ownThread.setDaemon(true); //a daemon-thread will not cause the program to stay alive if no other thread is running. ownThread.start(); } /** * starts or continues the ticker (this method is idempotent) */ public void play() { state = TickerState.PLAY; //notifyAll can only be called if it has the monitor synchronized (ownThread) { ownThread.notifyAll(); } } /** * pauses the ticker (this method is idempotent) */ public void pause() { state = TickerState.PAUSE; } /** * stops the ticker (this method is idempotent) */ public synchronized void stop() { stopped = true; } @Override public void run() { //again, we need to synchronize this on ownThread because ownThread.wait(); demands this. synchronized (ownThread) { while (!stopped) { while (state == TickerState.PAUSE) { try { ownThread.wait(); } catch (InterruptedException e1) { e1.printStackTrace(); } } if (System.currentTimeMillis() - lastSecond > 1000 * every) { // tick - a second p.tick(); lastSecond = System.currentTimeMillis(); } } } } }
The idea is to loop infinitely if the ticker is in PLAY, and to pause the whole thread if it is on PAUSE. On STOP, we end the infinity-loop in the run-method.
You also see that I use timestamps to trigger events not Thread.sleep(TIME) to do that. The reason is that Thread.sleep is only as accurate as the precision and accuracy of systemtimers and schedulers. By checking everytime if the delta-value of the system-time is greater or equal than one second, we should be very precise.
So what happens if we pause the timer?
The Thread checks in a loop every time if we are in paused state. If that is true, he puts himself to sleep.
while (state == TickerState.PAUSE) { try { ownThread.wait(); } catch (InterruptedException e1) { e1.printStackTrace(); } }
The call ownThread.notifyAll(); in play() wakes him up.
public void play() { state = TickerState.PLAY; //notifyAll can only be called if it has the monitor synchronized (ownThread) { ownThread.notifyAll(); } }
You can download the project from github:
https://github.com/philipphock/PechaKucha.git
Gleich vorweg: Wenn man Git alleine nutzt, braucht man natürlich überhaupt keinen Server. Und wem grafische Oberfläche ohnehin ein Fremdwort ist, der kann seinen Server sicher auch nur als reinen Git-Server betreiben. Für unser Team waren in der Programmentwicklung jedoch die Features, die auch GitHub bietet, sehr wichtig: Wir wollten Issues online erfassen, Commits direkt im Code kommentieren. Ja, vielleicht sogar MergeRequests direkt über die Web-Oberfläche abarbeiten. All dies wurde durch Gitlab ermöglicht, wenn auch zum Teil erst nach ein paar Versionssprüngen.
Versionssprünge? – Ja, Gitlab steckt so gesehen noch in den Kinderschuhen. Oder anders formuliert: Es wird stetig verbessert. Wer sich ein wenig mit Ruby auskennt oder einfach ein paar neue Ideen einbringen will, kann ja mal bei Gitlab auf GitHub vorbeischauen.
Wir fingen im März unter Nutzung der Version 2.2 an, mit Gitlab zu arbeiten. Mittlerweile läuft auf meinem Server die 2.6er-Version, in den kommenden Tagen müsste das Update auf 2.7 veröffentlicht werden. Anfangs weigerte ich mich, das System zu aktualisieren, um das Sopra nicht unnötig zu gefährden. Es zeigte sich jedoch schnell, dass jedes einzelne Update sehr nützliche Features mit sich brachte – so kamen seit Beginn unserer Arbeit die Möglichkeit der Milestones, Taggen von Issues und das Annehmen von MergeRequests über die Web-Oberfläche dazu.
Die Installation auf dem eigenen Server geht relativ einfach von der Hand. Eine Schritt-für-Schritt-Anleitung dazu gibt es im Gitlab-Wiki, die ich eigentlich nur abarbeiten musste. Offiziell unterstützt wird nur Ubuntu, es finden sich im Internet aber mittlerweile genügend Hilfen, wie man Gitlab auch auf CentOS- oder anderen Servern zum Laufen bekommt. Einmal installiert gehen Updates so einfach wie nur möglich von der Hand: Ein einfaches git pull
und rake db:migrate
reichen in aller Regel aus, um das System auf den neuesten Stand zu bringen.
Wie oben schon geschrieben: Im Prinzip bringt Gitlab alles mit, was man auch von GitHub kennt. Das große Vorbild schwingt in allen Diskussionen um neue Features mit. So birgt die Oberfläche von Gitlab erstmal auch kaum Überraschungen.
Da ich noch nie auf GitHub in einem Team gearbeitet habe, kann ich das leider nicht vergleichen. In Gitlab gibt es eine ganze Reihe an verschiedenen Rollen, vom “Guest” bis “Master”. Die Rollenverteilung auch im Team umzusetzen, erwies sich bereits nach wenigen Tagen gemeinsamer Arbeit am Sopra als sehr nützlich: Durch die Unterscheidung zwischen “Developer” und “Master” konnten nur noch zwei Mitglieder in unseren Master-Branch pushen und waren dementsprechend für das Mergen und Schließen der Issues hauptverantwortlich.
Letztlich nutzten wir im Team mit Ausnahme der sogenannten “Wall” alle Mittel, die uns Gitlab an die Hand gab. Am anschaulichsten wird das vielleicht, wenn man betrachtet, wie sich unser Workflow seit Benutzung von Gitlab geändert hat:
Grundsätzlich gab es also nichts, was uns noch gefehlt hätte. Nervig waren hin und wieder lediglich die kleinen Aussetzer, auf die ich im nächsten Abschnitt mal kurz eingehen werde.
Auch wenn sich Gitlab und GitHub nicht nur optisch sehr ähneln, verfolgen beide Systeme eigentlich unterschiedliche Ziele: Gitlab beschränkt sich komplett auf das Hosting von privaten Repositories. Es ist also nicht möglich, Gitlab als selbst gehosteten Ersatz für Github zu nutzen, und seine Open-Source-Projekte damit zu verwalten. Klar, prinzipiell geht das natürlich auch, aber dann kann eben niemand den Code sehen. Im Gegensatz zu GitHub bietet der Klon nämlich keine Möglichkeit, auf Projekte ohne Registrierung zuzugreifen. Und neue Benutzer anlegen kann nur der Admin. Auch wenn in sehr regelmäßigen Abständen von etwa 2 Wochen ein neuer Issue an die Entwickler gerichtet wird, dies doch umzustellen, bleiben diese ihrer Linie treu und grenzen sich damit ganz klar von Github ab. Und das auch ganz explizit mit der Aussage, dass Open-Source-Projekte eben am besten zentral auf GitHub gehostet werden.
Das Bearbeiten von MergeRequests direkt über die Weboberfläche ist zwar sehr komfortabel, sollte naturgemäß aber nur bei einfachen Änderungen benutzt werden. Ohnehin prüft Gitlab, bevor es die Möglichkeit anbietet, ob es zu irgendwelchen Merge-Konflikten kommt. Doch auch wenn dem nicht so ist, habe ich Abstand davon genommen, umfangreiche Änderungen über diesen Weg zu übernehmen. Letztlich schien mir der traditionelle Weg über git merge
und ggf. git mergetool
doch immer noch am sichersten.
Etwas Schluckauf kann man Gitlab im Moment auch noch durch den intensiven Gebrauch von Umlauten bereiten: Commit-Messages, die Umlaute beinhalten, können mitunter dafür sorgen, dass ein ganzer Dateibaum in der Weboberfläche nicht mehr verfügbar ist. Umlaute in Dateinamen sorgen dafür, dass die ansonsten sehr hilfreiche Funktion, um ein komplettes Repository als *.tar.gz herunterzuladen, plötzlich nicht mehr funktioniert. Schade ist, dass das System in diesen Fällen keine ordentliche Fehlerseite liefert, sondern schlicht mit 404 quittiert. Und man dann händisch versuchen muss, die Sachen in der Datenbank zu korrigieren.
Generell hat die Zahl der Fehler mit jedem Update aber gut abgenommen, sodass man sich einfach auf die Vermeidung von Umlauten einlassen sollte und ein sehr gutes und stabiles System bekommt.
Eine Demo gibt es ebenfalls auf den Seiten von Gitlab.
]]>Deswegen habe ich einen Notificationservice für die Piratenpads geschrieben. Das Skript loggt sich ein, fragt die aktuelle Version des Dokuments ab und speichert diese lokal zwischen. Beim nächsten Überprüfen (idealerweise als Cronjob) wird die lokale Kopie mit dem online-Original verglichen. Gibt es eine Differenz wird eine E-Mailbenachrichtigung mit dem diff
versandt.
Um das Ganze zu Skripten habe ich node.js mit der Bibliothek request verwendet:
Request is designed to be the simplest way possible to make HTTP calls. It support HTTPS and follows redirects by default.
Tolle Sache! Ohne HTTPS-Support kommen wir eh nicht weit, bei einigen Pads ist der Zugriff auf HTTPS eingeschränkt. Außerdem muss man sich dank der Bibliothek nicht um das mühsame Parsen von “Set-Cookie” Feldern kümmern. request übernimmt die Cookies standardmäßig einfach global für zukünftige Requests.
Um die Session zu initialisieren, also sich vor dem Login einen Cookie zu holen, sieht der Code etwa so aus:
var request = require('request'); var base = 'https://foo.piratenpad.de'; (function initiateSession() { request.get(base, function (error, response, body) { if (!error && response.statusCode == 200) { login(); } }); })();
Die Loginfunktion habe ich zusammengebaut, nachdem ich den kompletten Skriptablauf im Firefox durchgespielt habe und alle Requests mittels des (unheimlich praktischen) Live HTTP Headers Add-ons aufgezeichnet habe.
function login() { var options = { url: base + '/ep/account/sign-in', form: {'email': 'john@doe.de', 'password' : 'mypw'} }; request.post(options, function (err, res, body) { request.get(base + '/' + acc.padId, function (err, resp, body) { // get the latest version of the pad document var linkToLatestVersion = body.match(/[\w\d\/\-\.]*(latest')/gi); linkToLatestVersion = linkToLatestVersion[0].replace("'", ''); getLatestDocument(linkToLatestVersion); }); } ); }
Die aktuelle Version des Dokuments lässt sich dann mit einem einfachen GET-Request abfragen:
function getLatestDocument(linkToLatestVersion) { request.get(base + linkToLatestVersion, function (err, resp, body) { var start = body.search('id="padcontent">') + 'id="padcontent">'.length; var end = body.search("<!-- /padeditor -->"); var padContent = body.substring(start, end); // strip all tags and entities padContent = padContent.replace(/(<[^>]+>)|(&[#\w]+;)/gi, ''); console.log(padContent.trim()); }); }
Das Ganze zusammengefasst als ordentliches, sauber konfigurierbares, Projekt gibt es hier auf GitHub. Das Skript kann sehr einfach für ähnliche Aufgaben wiederverwendet werden. Als Anregung: Beispielsweise wäre es möglich das Uni Hochschulportal anzuzapfen um E-Mailbenachrichtigungen zu versenden, wenn neue Prüfungsergebnisse eingetragen sind.
Update: Ich habe noch die Möglichkeit hinzugefügt, Benachrichtigungen für Änderungen an dem Inhalt hinter einer URL zu konfigurieren (im Ordner simple-webpage/
). Ich benutze das als simple Lösung für Seiten, die keinen RSS-Feed bereitstellen.
Und wenn es erlaubt ist Aufgaben in einer so interessanten Sprache abzugeben, dann kann ich es mir natürlich nicht nehmen lassen dies auch zu tun. Ich begann also Whitespace zu lernen und war erstaunt, wie schnell man sich gute Kenntnisse in dieser Programmiersprache aneignen kann. Wer schon einmal mit einer Assembler-Sprache gearbeitet hat, sollte mit Whitespace keine Probleme haben. Innerhalb weniger Stunden hatte ich bereits einen Whitespace Disassembler (whdisasm) geschrieben gehabt, der mir Whitespace Quelltext in lesbare Mnemonics übersetzt. Disassembler ist natürlich eigentlich keine korrekte Bezeichnung für ein Programm mit dieser Funktionalität. Aber der Name klingt meiner Meinung nach einfach besser als „Whitespace-zu-Mnemonics-Konverter“ oder so. Mit dem whdisasm konnte ich die im Web existierenden Beispiele leichter analysieren und damit lernen, wie man die Sprache verwendet.
Ich schrieb einige kurze Programme. Aber das Coden mit Whitespace ist doch nicht gerade einfach (obwohl für viele Editoren Syntax Highlighting möglich ist). Also machte ich mich daran noch einen Assembler, den whasm, zu entwickeln. Auch whasm war in kurzer Zeit lauffähig. In nur zwei Tagen hatte ich also eine neue Sprache gelernt, einen Disassembler und einen Assembler, sowie einige Libraryfunktionen, geschrieben und meine Programmieraufgabe in Whitespace abgegeben.
Viele esoterische Programmiersprachen sind zwar auf den ersten Blick furchtbar abschreckend aber in Wirklichkeit sehr einfache Konstrukte. Whitespace hat zudem eine ausgezeichnete Dokumentation, die meine einzige Informationsquelle für whasm und whdisasm gewesen ist.
Ich verwende ab jetzt für Leerzeichen die Abkürzung [space], für Tabulatoren [tab] und für Zeilenumbrüche [lf]. Diese Darstellung wird auch im Whitespace Tutorial so verwendet.
Ich habe bereits erwähnt, dass Whitespace eine Art Assemblersprache ist. Der Whitespace Quelltext (also die Leerzeichen, Tabs und so) werden üblicherweise interpretiert. Im Netz sind dafür leicht verschiedene Interpreter zu finden. Bestimmt gibt es auch irgendwo einen Compiler (und wenn nicht, ist das hier vielleicht ein Anstoß dafür einen zu schreiben).
Whitespace kennt zwei Arten von Speicher: Stack und Heap. Für den Stack gibt es Operationen, wie „auf den Stack pushen„, „vom Stack poppen“, „duplizieren des obersten Elements“ und einige weitere. Im Heap können Daten an beliebigen Adressen gespeichert und gelesen werden. An dieser Stelle passt es vielleicht zu erwähnen, dass Whitespace keine Datentypen kennt, wie sie sonst üblich sind. Die einzige Unterscheidung ist „Zahl“ oder „(ASCII-) Zeichen“. Druckbare Zeichen haben dabei 8 Bit. Zahlen können beliebig lang(!) sein. Eine positive Zahl beginnt mit einem [space] und eine negative mit einem [tab]. Ansonsten ist ein [space] die binäre 0 und ein [tab] die binäre 1.
Hier ein Beispielprogramm, dass zwei Zahlen aus dem Heap liest, diese addiert und das Ergebnis auf dem Bildschirm anzeigt. Ich habe für bessere Lesbarkeit statt echten Leer-/Tab-/Zeilenumbruchszeichen sichtbare Abkürzungen verwendet ;)
// Achtung! Zeilenumbrüche sind nur da, wo [lf] steht. Ich habe die Zeilen im Beispiel
// lediglich für bessere Lesbarkeit umgebrochen.
// Zuerst wollen wir zwei Zahlen vom Heap auf den Stack laden.
// Um eine Zahl zu laden, pushen wir zunächst ihre Adresse auf den Stack
[space] // das erste [space] bedeutet, dass wir den Stack manipulieren wollen
[space] // das zweite [space] sagt, dass wir die Zahl nach dem Befehl auf den Stack pushen wollen
[space][tab][tab][tab][lf]
// [space][tab][tab][tab] ist die binäre 0111 (also 7)
// das [lf] markiert das Ende des Zahlenparameters
[tab][tab] // [space] sagt, dass wir aus dem Heap lesen wollen (an der Adresse im Stack)
[space] // [tab][tab] bedeutet, dass wir auf den Heap zugreifen
// Das Selbe nochmal mit der zweiten Zahl:
[space][space][space][tab][space][space][space][lf] // diesmal die Adresse 8 (01000)
[tab][tab][space]
[tab][space] // leitet eine arithmetischen Befehl ein
[space][space] // Addition der zwei obersten Werte auf dem Stack
[tab][lf] // beginnt eine I/O Operation
[space][tab] // Bildschirmausgabe der Zahl oben auf dem Stack
[lf][lf][lf] // beendet das Programm
Das selbe Programm würde in (mit whasm übersetzbaren) Mnemonics so aussehen:
push 7
retrieve
push 8
retrieve
add
printnum
end
Esoterische Programmiersprachen machen Spaß! Das ist zumindest meine Meinung. Ich habe in der Zeit, in der ich mich damit beschäftigt habe, einiges gelernt. Nicht nur Whitespace selbst, sondern den Umgang mit stackbasierten Sprachen oder auch, wie leicht sich solche Konstrukte parsen lassen.
Der Code für whasm und whdisasm sind auf github verfügbar. Dort habe ich auch begonnen Libraryfunktionen zu implementieren, die mit whasm in Whitespace übersetzt werden können.
]]>