Pelzvieh

Was wollte ich gleich nochmal sagen?

Gasheizungen sind ja nicht mehr so der letzte Schrei der Technik. Aber wenn du eine hast, solltest du dich regelmäßig versichern, ob sie eigentlich noch so funktioniert, wie sie soll. Ich berichte hier 1:1 von meinen Erfahrungen als ganz normaler “Anwender” einer solchen Heizung. Die Anlage wurde übrigens jährlich von einer Fachfirma gewartet und selbstverständlich wurden die hier beschriebenen Fehler auch von der Fachfirma behoben. Fast nichts davon ist aber in der Inspektion aufgefallen, ganz einfach weil sich diese auf den Brennvorgang konzentriert und nicht die Wechsel der Betriebszustände im Normalbetrieb analysiert – das würde auch Stunden dauern.

Der Außentemperaturfühler

Geh mal an deine Heizungsanlage und lass dir die Messungen des Außentemperaturfühlers anzeigen. Machen die Angaben Sinn?

Bei mir Folgendes: Temperatur 5,0°C, Minimium 5,0°C, Maximum 5,0°C. Das hatte nichts mit der Lufttemperatur zu tun, gar nichts. Und die Temperatur ändert sich bei uns auch zwischen Morgen und Nachmittag.

Finding: der Außentemperaturfühler ist kaputt!

Maßnahme: Austausch des Temperaturfühlers.

Nach dem Tausch des Außentemperaturfühlers: In einer Frostnacht Minimum 8,9°C. An einem warmen Frühlingstag Maximum 14,3°C.

Finding: der Außentemperaturfühler lungert in einem Lichtschacht, dessen Mikroklima mit den realen Wetterverhältnissen nichts zu tun hat.

Maßnahme: längeres Anschlusskabel beschaffen, aus dem Lichtschacht herausführen und an einer repräsentativen Stelle der Hauswand anbringen lassen.

Die Temperaturkurve der Vorlauftemperatur

So im Normalbetrieb: geben deine Heizkörper ein sanftes Rauschen von sich, oder dringt ein gepresstes Zischen aus dem Ventil?

Im Normalbetrieb sollten die Ventile der Heizkörper typischerweise offen sein (solange nicht Sonneneinstrahlung oder andere Wärmequellen die Temperatur hochgetrieben haben), der Heizkörper über seine ganze Höhe eine gleichmäßig abnehmende Temperatur haben.

Hier fast geschlossene Ventile, Heizkörper ganz oben ein paar Zentimeter sehr heiß. Und der einzige Befund bei der Wartung: ein knochentrockener Brenner statt kondensierender Feuchtigkeit wie sich das für eine Brennwerttherme gehört.

Befund: als Ausgleich für den kaputten Außenfühler wurden die Temperaturvorgaben völlig verdreht.

Maßnahme: drehe die Ziel-Raumtemperatur an der Therme radikal zurück (hier ungefähr 5 Grad). Mechanische Thermostat-Ventile im Gegenzug etwas weiter auf.

Das 3-Wege-Ventil

Geh mal duschen, aber während aktiver Warmwasserbereitschaft. Also sprich: bring deine Heizung dazu, Warmwasser zu bereiten. Was machen derweil die Heizkörper?

Hier: die Heizkörper laufen weiter und werden richtig heiß, auch wenn es draußen nicht besonders kalt ist.

Befund: das 3-Wege-Ventil ist kaputt.

Maßnahme: tausch das kaputte 3-Wege-Ventil, damit die Heizanlage wieder zwischen Warmwasserbereitung und Raumheizung umschaltet.

Das Rohr vom Speicher

Das 3-Wege-Ventil ist frisch getauscht, nach dem Duschen gehen die Heizkörper aus – aber stundenlang nicht wieder an?! Am Gerät liest man: der Kessel ist auf Maximaltemperatur, der Brenner aus, die Warmwassertemperatur dagegen: kalt. Dabei kommt durchaus warmes Wasser aus dem Hahn. Warum nur wird der Brenner die Wärme nicht los und das Wasser nicht warm?

Befund: ein Rohr ist verschwunden. Korrodiert, in den Speicher gefallen oder gleich ganz aufgelöst. Klingt spooky, ist aber so.

Maßnahme: neues Rohr einbauen.

Was bringt eine funktionierende Heizanlage?

Ergebnis: über 50% Einsparung im Gasverbrauch, geringerer Verschleiß.

Und jetzt schau nochmal auf deine Vorlauftemperaturen im Winter und lasse mit den Werten prüfen, ob bei dir eine Wärmepumpe funktioniert.

Wie schon geschrieben, mein #BananaPi M2 Zero konnte ja nicht mit dem DHT11-Kernelmodul den angeschlossenen Sensor auslesen. Von rund 80 erwarteten Flanken lösten gerade immer knapp 20 einen IRQ aus. Ich habe auch schon Workarounds durch Polling im Userspace und eine polling-Version des DHT11-Kernelmoduls vorgestellt. Die gute Nachricht: der Mist kann weg!

Diskussionen mit den Kernel-Entwicklern

Fruchtbare Anstöße gab es, als ich versucht habe, den Polling-Driver in den Kernel zu bekommen. Das dürfe eigentlich gar nicht nötig sein, nicht bei diesem Board, hieß es. Es müsse ein Problem mit meinem Setup geben. Nun, zum Glück ist das Setup ja aus nachvollziehbaren Quellen mit reproduzierbaren Schritten entstanden, da weiß man ja, wo's her kommt.

Fakten, Fakten, Fakten

Ich habe also analysiert, was eigentlich beim Treiber so ankommt. Allerdings sind wir jetzt am Puls der Kernel-Zeit, d. h. ich musste erstmal den aktuellen staging-testing-Branch des Kernel für die Banane übersetzen (Image, Module, devicetree) und ein initramfs-Image generieren. Letzteres verursacht wieder Puls, weil der ARM64-Laptop nicht in der Lage ist, armhf-Binaries auszuführen. Aber dank update-ramfs auf dem BananaPi selbst, konnte auch dieses Problem wieder gelöst werden. Meine neue Erkenntnis des Tages: “dynamic debug” ist eine Kernel-Funktion, über die man ganz bestimmte debug-Ausgaben im laufenden Kernel an- und ausknipsen kann:

    echo "file dht11.c +p" | sudo tee /proc/dynamic_debug/control

..schaltet das Debugging unseres dht11-Kernelmoduls an. Ergebnis der Übung eigentlich recht diffus: so alle 150-300µs trudelt mal ein IRQ ein, über die Hälfte geht verschütt. CPU-Leistung, Interrupt-Geschehen, Speicher, Kernel-Meldungen: alles unauffällig. Was ist hier los?

Des Bananenproblems Kern

Ich weiß gar nicht so genau warum, aber ich hatte immer das Gefühl, dass das Problem mit der Verarbeitung von GPIO-Signalen zu IRQs des SoC zu tun haben muss – und nicht mit etwas, was Betriebssystem und CPUs so treiben. Diesem Gefühl folgend arbeitete ich mich mäßig inspiriert durch ein Datasheet des Allwinner H3 (zu finden in den Untiefen des Internet...). Architektur des SoC, Busse, Bridges. Beschreibung der GPIOs, Register der PA-Bank, hmhm. Oha! Hinter den Registern der PG-Bank kommt nochmal die PA-Bank dran: Kapitel 4.2.55ff beschäftigen sich mit Registern zur Kontrolle der Interrupts aus der PA-Bank. Und dann fällt mir ins Auge “4.22.2.61. PA External Interrupt Debounce Register”. Debounce, also ein Filter gegen Interrupt-Feuer durch prellende (mechanische) Taster, sowas kann das Gerät? Und der Default ist Abriegelung mit 32kHz, scheinen die kargen Infos nahe zu legen. Das könnte die Erklärung sein und war dann auch! Geschwind in Devicetree-Doku und Kernel-Sourcen geblättert, wie dieses Register bespielt wird und flugs den Devicetree-Overlay ergänzt um eine Einstellung des &pio:

   input-debounce = <5 0>;

...und schon funktioniert der Treiber!

Und nun?

Ihr findet das aktualisierte Devicetree Overlay in meinem Repository von Bananen-Ressourcen. Daneben auch einen veränderten DHT11-Devicetreiber, der nur noch auf falling edges lauscht, da die low-Pegel des Sensors keine Information tragen. Der ist kein Muss, aber meinen Messungen nach funktioniert er noch einen Tick zuverlässiger als der Original-Treiber und letzter benötigt einen eher noch niedrigeren debounce-Eintrag: mit input-debounce = <1 0> tut er's dann auch ganz robust. Solltet ihr die Polling-Version des Treiber gebaut und per device tree eingebunden haben: das kann jetzt glücklicherweise entfallen. Was mir noch unklar ist: ob das Fehlen einer input-debounce-Konfiguration nicht nachgerade ein Bug im Devicetree (aus den Kernel-Sourcen) ist. Denn an der A-Bank hängen auch andere Geräte, nicht nur über die Steckerleiste frei nutzbare GPIOs. Dass z. B. die seriellen Schnittstellen mit dieser Schaumbremse glücklich sein sollen, kann ich mir nur schwer vorstellen. Und seit der Umstellung ist die Kerneltask sugov:0, die mich vorher in der Anzeige von top wegen ihres (angeblichen) CPU-Konsums verwirrt hat, von dort verschwunden. Die Änderung ist also alles andere als frei von Nebenwirkungen, schauen wir mal, wie sie sich bewährt.

Diesen Folgeartikel habe ich auf Englisch geschrieben und folgerichtig auf dem Furrycreature-Blog veröffentlicht.

GPIO, aber wie?

Wenn es darum geht, auf den Einplatinencomputern elektronische Komponenten anzusteuern bzw. auszulesen, wird man sowohl beim #RaspberryPi als auch beim #BananaPi auf Libraries und Dämonen im Userland gestoßen: #pigpiod, #WiringPi und ähnliche. Schaut man sich mal oberflächlich an, wie diese gebaut sind, kriegt man es mit der Angst: Direkte Registerzugriffe per /dev/mem werden munter gemischt mit Kernelfunktionen verwendet, eine Prüfung ob ein Pin bereits von einem Kernelmodul bedient wird, findet nicht statt. Z. B. stellte ich erst im Zuge meiner BananaPi-Rechere fest, dass ich auf einem RaspberrPi einen Temperatursensor per gigpiod zugreife, während gleichzeitig das OneWire-Kernelmodul darauf lauscht. Im Kernel und im klassischen Linux-Userland gibt es dagegen auch fertige Lösungen (irgendwie weniger präsent in meiner favorierten Suchmaschine): das neue GPIO-Kernel-Interface unter /sys/bus/gpio/devices/gpiochip0 kann

  • zum einen bastlerisch-explorativ mit den Tools aus dem Paket gpiod genutzt werden (also Achtung: gpiod ist etwas völlig anderes als pigpiod, die bauen nicht aufeinander auf, sondern beharken sich),
  • zum anderen gibt es fertige Kernelmodule für bestimmte Hardware, die man an diese GPIO-Pins hängt (genau für sowas wurde das IIO-Framework im Kernel bereit gestellt).

Das Beispiel”projekt”

Ich nehme hier mal als Beispiel einen Temperatur- und Feuchtigkeitssensor #DHT22. Dieser wird an einen PIN angeschlossen und es gibt einen IIO-basierten Kernel-Treiber dht11.ko (DHT11 und DHT22 sind offensichtlich sehr nahe verwandt und dht11.ko kann auch DHT22). Spoiler: das Beispiel ist fies gewählt, denn der dht11.ko funktioniert am BananaPi am Ende des Tages nicht. Ich hoffe, dass einer von euch weiß, was das Problem verursacht und wie man es abgestellt bekommt :-D

Anschluss des Sensors

Mein DHT22 kommt auf einer kleinen Platine konfektioniert mit 3 beschrifteten Anschlüssen nebst passendem 3-poligen Kabel. Der BananaPI hat im Auslieferungszustand ein 40-poliges Lochraster, in das ich eine Sockelstiftleiste eingelötet habe (vermutlich gibt es auch Sockelstiftleisten mit Klemmkontakten). Das Anschlussschema findet man im BananaPi-Wiki. Der (+)-Anschluss muss mit einem 3,3V-Pin verbunden werden, Pin Nr. 1 bietet sich an. Der (–)-Anschluss mit einem Gnd-Pin, z. B. Pin Nr. 6. Der GPIO-Pin kann grundsätzlich aus einer reichlichen Auswahl gewählt werden, wir nehmen Pin Nr. 7, gemäß Schema Anschluss PA6. Also nicht verwechseln: wir stöpseln die Hardware an Pin 7, in der Software heißt das Ding aber PA6 (A=Bank 0, 6=Nr. 6 auf dieser Bank).

Konfiguration von dht11.ko – das Devicetree Overlay

Versucht man sich diesem dht11.ko naiv zu nähern – ich hätte erwartet, dass das Kernelmodul irgendwelche Parameter bietet, über das man ihm mitteilt, an welchem Pin das Gerät sitzt – aber weit gefehlt, so tickt diese IIO-Welt nicht: sie möchte, dass wir über den #Devicetree kundgeben, welche Geräte wo an welchen Bussen hausen. Der eigentliche Devicetree des BananaPi findet sich bereits in den Sourcen des Linux-Kernel. Er beschreibt sozusagen, welche Geräte auf der kleinen Platine wie zusammengestöpselt sind (sogar die auf dem SOC integrierten Geräte). Wir gedenken aber nun, ein weiteres Gerät hinzuzufügen, indem wir es an einen GPIO-Pin anschließen. Diese Information müssen wir also in den Devicetree einbauen. Da wäre es natürlich praktisch, wir könnten genau die Aussage, “wir haben ein DHT11 an PA06 gesteckt” hinterlegen, ohne uns mit dem ganzen Devicetree auseinandersetzen zu müssen? Genau dafür gibt es Devicetree Overlays. Damit das schick funktioniert, habe ich in der Anleitung zur Erstellung des Boot-Images beim Kompilieren des Devicetree die Option DTC_FLAGS=-@ hineingeschmuggelt. So enhält der kompilierte Devicetree die logischen Symbole und wir können im Devicetree Overlay darauf referenzieren. So sieht das dann aus (du findest die Datei auch auf Github):

// Definitions for dht11 module
/*
Adapted from dht11.dts for Raspberrypi, by Keith Hall
Adapted by pelzi.
*/
/dts-v1/;
/plugin/;

/ {
        fragment@0 {
                target-path = "/";
                __overlay__ {
                        temperature_humidity: dht11@6 {
                                compatible = "dht22", "dht11";
                                pinctrl-names = "default";
                                pinctrl-0 = <&dht11_pins>;
                                gpios = <&pio 0 6 0>; /* PA6 (PIN 7), active high */
                                status = "okay";
                        };
                };
        };

        fragment@1 {
                target = <&pio>;
                __overlay__ {
                        dht11_pins: dht11_pins {
                                pins = "PA6";
                                function = "gpio_in";
                                bias-pull-up;
                        };
                };
        };

        __overrides__ {
                gpiopin =       <&dht11_pins>,"pins:0",
                                <&temperature_humidity>,"gpios:8";
        };
};

Das “Fragment 0” erzeugt einen neuen Knoten “dht11@6” und dem Symbol “temperature_humidity” direkt an der Baumwurzel. Es referenziert auf dht11_pins als pinctrl-0. Diese erzeugt das “Fragment 1”, und zwar dort wo das Gerät auch hängt, nämlich am PIO-Controller, also unterhalb des Knotens mit dem Symbol pio. Es gibt an, an welchen Pins es hängt (pins, nur einer, mit dem Symbol PA6). Schließlich definieren wir noch eine Parametrierung gpiopin, um einen anderen Pin angeben zu können. Aus dieser Definition kompilieren wir ein binäres Devicetree-Overlay:

dtc dht11-banana.dts -@ -o dht11-banana.dtbo

Erzeugen und Einspielen des zusammengesetzten Devicetree

Während das Bootsystem des RaspberryPi bereits eine Nachlade- und Parametrierungslogik implementiert (/boot/config.txt), haben wir solche Bequemlichkeit auf dem BananaPi (noch?) nicht. Man kann durchaus dem U-Boot-Loader sowohl Devicetree, als auch separate Overlays zum Laden und zusammenmischen antragen. Da dies aber ohnehin hart codiert auf dem Image ist, bevorzuge ich aktuell, den Devicetree im Vorfeld fertig zu erzeugen und aufzuspielen, da ich Fehler nicht erst im Rahmen des Bootvorganges auf einer seriellen Console zu Gesicht bekomme, sondern als gewöhnliche Meldung eines gewöhnlichen Kommandozeilentools. Auftritt fdtoverlay!

fdtoverlay -v -i sun8i-h2-plus-bananapi-m2-zero.dtb -o banana-with-dht.dtb dht11-banana.dtbo

Dieser Aufruf erzeugt aus dem gewöhnlichen Devicetree des BananaPi M2 Zero, sun8i-h2-plus-bananapi-m2-zero.dtb und dem gerade erstellten Overlay dht11-banana.dtbo einen vollständigen Devicetree mit DHT-Knoten namens banana-with-dht.dtb. Dies kopiere ich einfach auf das Image über den bisher genutzten Devicetree:

cp banana-with-dht.dtb /mnt/debinst/boot/dtbs/sun8i-h2-plus-bananapi-m2-zero.dtb

Diese Übungen kann man selbstmurmelnd auch auf dem laufenden Device selbst durchführen und das Ergebnis dann nach /boot/dtbs/sun8i-h2-plus-bananapi-m2-zero.dtb kopieren – aktiv wird es aber erst nach einem Reboot. Ich habe leider kein Userspace-Kommando à la dtoverlay auf dem RaspberryPi finden können, um den Devicetree auf dem laufenden Gerät zu modifizieren. Das ist erstaunlich, weil es dafür eigentlich eine Kernel-API gibt, aber dtoverlay funktioniert tatsächlich nur auf einem RaspberryPi. Wer möchte ein solches Tool implementieren?

Neustart und Nutzung

Bootet man nun den BananaPi mit diesem ergänzten Devicetree, bemerkt man sofort ein geladenes Kernelmodul

$ lsmod|grep dht
dht11                  20480  0
industrialio           65536  1 dht11

Und es ist fein säuberlich hinterlegt, dass und wofür wir unseren Pin PA6 verwenden:

$ sudo gpioinfo
gpiochip0 - 224 lines:
[...]
	line   6:      unnamed    "dht11@6"   input  active-high [used]
[...]

Der DHT-Treiber stellt uns fertige Geräte zum Auslesen zur Verfügung:

$ ls -l /sys/bus/iio/devices/iio\:device0/
insgesamt 0
-r--r--r-- 1 root root 4096 15. Jan 13:16 dev
-rw-r--r-- 1 root root 4096 15. Jan 13:16 in_humidityrelative_input
-rw-r--r-- 1 root root 4096 15. Jan 13:16 in_temp_input
-r--r--r-- 1 root root 4096 15. Jan 13:16 name
lrwxrwxrwx 1 root root    0 15. Jan 13:16 of_node -> ../../../../firmware/devicetree/base/dht11@6
drwxr-xr-x 2 root root    0 15. Jan 11:30 power
lrwxrwxrwx 1 root root    0 15. Jan 13:16 subsystem -> ../../../../bus/iio
-rw-r--r-- 1 root root 4096 14. Jan 23:08 uevent

Wir können die aktuellen Messwerte einfach durch Lesen an den device files in_humidityrelative_input und in_temp_input ermitteln.

Das Problem mit dht11

Wie oben schon angekündigt, gibt es hier aber leider ein verdrießliches Problem – das Auslesen funktioniert nicht:

$ cat /sys/bus/iio/devices/iio\:device0/in_humidityrelative_input 
cat: '/sys/bus/iio/devices/iio:device0/in_humidityrelative_input': Die Wartezeit für die Verbindung ist abgelaufen

Parallel dazu nennt uns dmesg:

[52801.685052] dht11 dht11@6: Only 18 signal edges detected

Wie ich nach etwas Analyse definitiv bestätigen kann: der Treiber weckt den DHT11-Sensor auf und dieser fängt an, seine Messwerte zu schicken. Das Dumme ist, dass der BananaPi nur ein Bruchteil der tatsächlich entstandenen Flanken am Signalpin via Interrupt einfängt. Wir wissen von den eingefangenen Flanken zwar hinreichend genau die Zeitstempel, aber es fehlen halt ungefähr 70 Flanken! Warum es so lange Totzeiten zwischen den Interrupts gibt, ist mir noch nicht klar. Für Hinweise wäre ich extrem dankbar!

Was funktioniert denn nun?

Wenn du z. B. nur ein paar Ein-Aus-Sensoren (auch Bewegungsmelder...) und LEDs verbinden möchtest, funktioniert das auf analoge Weise durchaus. Du kannst analog des fragment@1 oben Pins für Input oder Output konfigurieren, inkl. Pull-Up/Down-Widerständen (Input) oder Drive-Konfiguration (Output). Und es gibt eine riesige Menge an IIO-Device-Treibern fertig in den Linux-Quellen, die eigentlich nur darauf warten in eigenen Projekten genutzt zu werden. Wie du den Treiber eines GPIO-basierten Gerätes am BananaPi in Betrieb bekommst, sollte dieser Artikel dir jetzt verraten haben. Ich wäre dir dankbar, wenn du erfolgreiche Anbindungen von anderen Sensoren auch veröffnetlichen würdest!

Vertiefende Dokumentation

  1. Device Tree Usage
  2. Device Tree Overlay Notes
  3. Bindings der IIO-Treiber

Warum die Übung?

Die etablierten Informationsquellen zum BananaPi (z. B. das Wiki) verlinken einmalig fertig gebaute Images und bieten Anleitung, den Rechner auf dieser Basis in Betrieb zu nehmen.

Warum ist das ein Problem?

  1. Vulnerabilities: die Images wurden von irgendeinem vergangenen, Jahre alten, Releasestand von Linux-Kernel und Distributionen gebaut. Inzwischen dürften hunderte von Schwachstellen dieses Standes bekannt geworden sein, deren Korrekturen aber nicht in den Images enthalten sind.
  2. Unklare Herkunft: es ist nicht transparent, wer diese Images gebaut hat, welche Review-Prozesse stattgefunden haben und welche Änderungen an den Upstream-Quellen durchgeführt wurden und zu welchem Zweck.
  3. Mangelnde Professionalität: unter den veröffentlichten Images befinden sich solche, die nicht durch einen Buildprozess erzeugt wurden, sondern einen Snapshot eines manuell aufgesetzten und genutzten Systems darstellen. Es finden sich insbesondere lange Shell-Histories, konfigurierte Zugänge zu WLANs taiwanesicher(?) Shared Office-Standorte.
  4. Fehlende Versionskontrolle: es werden auf unterschiedlichen Seiten unterschiedliche Stände auf unterschiedlichen Filesharing-Plattformen referenziert, von denen einige nicht mehr richtig funktionieren. All dies zusammen genommen macht diese Quellen zu einem El Dorado für persistente Angriffe (APTs). Sowas kommt mir nicht ins Haus und euch hoffentlich nicht ins Unternehmen. Da sind wir uns doch einig..?!? Dann kann's ja losgehen!

Ziele

  1. Ein Image zum Aufspielen auf Micro-SD-Karte wird erstellt
  2. Ein #BananaPi M2 Zero kann von einer mit diesem Image bespielten SD-Karte booten
  3. Der Bootvorgang verläuft unfallfrei in ein nutzbares System
  4. Funktionsfähig sind zumindest: Mini-HDMI (Bildschirmausgabe), Micro-USB-Port, GPIO (PINs), WLAN
  5. Das Gerät bucht sich automatisch in das konfigurierte WLAN ein und ist remote über ssh erreichbar (denn das Gerät hat keine fertig konfektionierte Ethernet-Schnittstelle und ich bin zu faul um die dafür nötigen PINs einzulöten und an per Kabeln an eine Ethernet-Buchse zu fummeln).

Anlegen des Images

Wir legen das spätere Image als lokale Datei auf einem Debian-Linux-Rechner (gerne virtuell...) an, machen diese via Loop-Device als Blockgerät verfübar und richten dieses richtig ein. Dieses Vorgehen hilft gegen die Versuchung, die SD-Karte zur Unzeit schonmal ins Zielgerät einzulegen und darauf manuell weiter zu fummeln, bis etwas funktioniert. Das Vorgehen hier folgt eigentlich nur Schritt für Schritt der entsprechenden Anleitung von Debian! Wenn dieser Artikel schon einige Jahre alt ist wenn du das liest, solltest du dort nach einer aktuellen Anleitung suchen. (todo: Hier fehlt eine Aufstellung der auf dem Linux-Rechner benötigten Pakete und sonstigen Voraussetzungen; beispielsweise fehlt die Warnung, dass das alles nicht auf arm64-Umgebungen funktioniert, weil ...)

dd if=/dev/zero of=bananapi_debian.img bs=1G count=3
sudo losetup -f bananapi_debian.img 
sudo fdisk /dev/loop0 
sudo mkfs.ext4 -O ^metadata_csum,^64bit /dev/loop0p1
sudo losetup -d /dev/loop0 
sudo losetup -P -f bananapi_debian.img 
sudo mkfs.ext4 -O ^metadata_csum,^64bit /dev/loop0p1 
sudo mkdir /mnt/debinst
sudo mount /dev/loop0p1 /mnt/debinst/
sudo debootstrap --arch=armhf --foreign --include=binfmt-support,wpasupplicant,dhcpcd5 bullseye /mnt/debinst
sudo cp /usr/bin/qemu-arm-static /mnt/debinst/usr/bin/
sudo LANG=C.UTF-8 chroot /mnt/debinst qemu-arm-static /bin/bash

An dieser Stelle haben wir die benötigten Softwarepakete schon in unserem Image liegen, wir können Programme auf der Zielplattform ausführen und sind gerade in die Systemumgebung unseres aufzusetzenden Rechners geschlüpft. Weiter geht's, nun im chroot:

/debootstrap/debootstrap --second-stage
editor /etc/adjtime

Das ist jetzt ein bisschen doof, weil du nicht siehst, was man da reinschreiben kann. Aber du kannst die Man-Page bzw. die Debian-Anleitung zu Rate ziehen :–)

dpkg-reconfigure tzdata
editor /etc/systemd/network/eth0.network
editor /etc/systemd/network/wlan0.network
editor etc/wpa_supplicant/wpa_supplicant.conf

Dokumentation nebst Beispielen findest du in der man-Page wpa-supplicant.conf

mkdir root/.ssh
editor root/.ssh/authorized_keys 

Hier kopierst du den public ssh-key rein, mit dem du dich am laufenden System anmelden willst. Achtung, es ist KEIN Passwort-Login für root möglich! Vielleicht möchtest du auch direkt an dieser Stelle einen User anlegen, für diesen einen ssh-Key hinterlegen, ihn für sudo berechtigen usw. Das halte ich für eine gute Idee, ist aber normales Linux-Alltagsgeschäft, da brauchst du ja keine Tipps von mir.

editor etc/resolv.conf
apt install openssh-server
editor etc/apt/sources.list.d/security.list # hier trägst du die apt source für Security-Updates deiner Distribution ein
editor etc/apt/sources.list.d/firmware.list # hier trägst du die apt source ein, aber die Sektion non-free
apt update
apt install locales && dpkg-reconfigure locales
apt install console-setup && dpkg-reconfigure keyboard-configuration
apt install linux-image-armmp-lpae
apt install firmware-linux bluez-firmware firmware-atheros firmware-bnx2 firmware-bnx2x firmware-brcm80211 firmware-cavium firmware-ipw2x00 firmware-iwlwifi firmware-libertas firmware-qcom-soc firmware-qlogic firmware-ti-connectivity firmware-zd1211 

Viel hilft viel! Naja, ehrlich gesagt müsste sich mal jemand die Mühe machen herauszusuchen, welche Firmware-Pakete der BananaPi wirklich braucht...

systemctl add-requires wpa_supplicant.service systemd-networkd-wait-online.service
systemctl add-wants network-online.target wpa_supplicant.service
editor /lib/systemd/system/wpa_supplicant.service

Im ausgelieferten Zustand würde wpa_supplicant nur über dbus lauschen, daher musst du -iwlan0 -c/etc/wpa_supplicant/wpa_supplicant.conf zu ExecStart hinzufügen. Kennst du eine saubere Lösung für das Problem?

echo "banana" > /etc/hostname
editor /etc/dhcpcd.conf # enable option "hostname"
exit

Nun sind wir wieder draußen aus dem Image. Die Userland ist jetzt fertig. Was fehlt, sind Bootloader und die Systemkonfiguration in /boot. Das U-Boot (den Bootloader) bauen wir wie folgt:

git clone git://git.denx.de/u-boot.git
cd u-boot
make bananapi_m2_zero_defconfig
make CROSS_COMPILE=arm-linux-gnueabihf-
cd ..

Eine Device-Tree-Definition für den BananaPi M2 Zero gibt es glücklicherweise fertig im Linux-Sourcetree. Wir können sie also ebenfalls bauen:

apt source linux-image-5.10.0-20-armmp-lpae
cd linux-5.10.*/
make CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make CROSS_COMPILE=arm-linux-gnueabihf- DTC_FLAGS=-@ dtbs
cd ..

Eine Bootloader-Konfiguration leihen wir uns von TuryRx in github aus. Das U-Boot-Script wird allerdings fest codierte Namen für Kernel und InitRAMFS verwenden, während diese Dateien im Debian-Paket mit Versionsnummern verziert sind. Deshalb kopieren wir die gerade installierten einfach.

wget https://github.com/TuryRx/Banana-pi-m2-zero-Arch-Linux/raw/master/boot.cmd
mkdir /mnt/debinst/boot/dtbs
sudo cp u-boot/u-boot-sun8i-h2-plus-bananapi-m2-zero.dtb /mnt/debinst/boot/dtbs/sun8i-h2-plus-bananapi-m2-zero.dtb
sudo cp /mnt/debinst/boot/vmlinuz-5.10.0-20-armmp-lpae /mnt/debinst/boot/zImage
sudo cp /mnt/debinst/boot/initrd.img-5.10.0-20-armmp-lpae /mnt/debinst/boot/initramfs-linux.img
sudo mkimage -A arm -O linux -T script -C none -a 0 -e 0 -n "BananaPI boot script" -d boot.cmd /mnt/debinst/boot/boot.scr
sudo umount /mnt/debinst 
sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/loop0 bs=1024 seek=8 # ja, so habe ich auch gekuckt: so lädt die Firmware des BananaPi ihren Bootloader...
sudo losetup -d /dev/loop0

Fertig ist das Image! Du kannst es jetzt auf eine Micro-SD-Karte übertragen und einen BananaPi M2 Zero damit starten.

UDP, Connected Socket-Magie, oder warum DTLS eigentlich funktioniert

  1. Februar 2020

Bei TCP ist ja alles klar. Du bindest deine Server-Socket an IP und Port und bei eingehenden Verbindungen liefert Accept die dazugehörige verbundene Socket.

Bei UDP gibt es keine entsprechende accept-Funktion. Schließlich kennt UDP ja auch keine Verbindung, hätte ich gesagt. Allerdings gibt es ja sehr wohl eine connect-Funktion, das hätte misstrauisch stimmen müssen.

Tatsächlich kann man mit UDP-Sockets dasselbe machen, wie mit TCP-Sockets, nur im Handwaschgang:

  1. Binde eine UDP-Socket an IP und Port, dies wird die Accept-Socket für neue Verbindungen. SO_REUSE_ADDR muss gesetzt sein, sonst funktioniert Schritt 3 nicht.
  2. Lies (receive) ein Paket. Du bekommst nur Pakete von Clients (identifiziert durch UDP/IP/Port), für die du Schritt 3 nicht ausgeführt hast. Neue Verbindungen in unserem Sinn also. Das Paket musst du aufheben.
  3. Erzeuge eine neue Socket, setze SO_REUSE_ADDR, binde sie an dieselbe IP/Port und verbinde (connect) sie mit der Clientadresse, die du aus dem ersten Paket ausliest.
  4. Verarbeite das in 2. gelesene Paket als erstes Nutzpaket deines Protokolls. Lies und schreib die weiteren Pakete über die in 3. erzeugte Socket. Sie, und nur sie, erhält die Pakete dieses Clients.
  5. Das Ende der Verbindung kann nur auf dem Application Layer festgestellt werden.

Der große Unterschied zu TCP ist also, dass eine Verbindung mit dem Empfang des ersten Paketes Nutzlast beginnt, es also kein accept ohne Nutzlast gibt. Ansonsten kann eine über bind und connect verbundene Socket gezielt für diese Verbindung genutzt werden.

Damit erklärt sich auch die obskure DTLS API von BouncyCastle! Die „accept-artige“ Schleife zum Aufbau von Verbindungen muss man selbst entwickeln. Dabei muss das empfangene Paket gleich als gültige DTLS-Eröffnung validiert werden. Dieses Paket übergibt man dann an die accept-Methode des Protokollobjektes, damit diese sie verarbeiten kann. Das Server-Objekt konstruiert man aus der neuen Socket aus Schritt 3.

Natürlich ist das Vorgehen nicht immer sinnvoll. UDP wird ja gerne für einfache Benachrichtigungen oder gelegentliche Anfragen (z. B. DNS) genutzt. Da ist es Ressourcenverschwendung, Sockets für die Clients zu erzeugen. Authentifizierung sollte man hier besser in den Paketen und nicht mit DTLS lösen.

Mit sachdienlichen Hinweisen von http://openssl.6102.n7.nabble.com/DTLS-with-multiple-clients-td73996.html und https://hacked10bits.blogspot.com/2014/12/udp-binding-and-port-reuse-in-linux.html

Die SQL-Injection

Warum zum Henker gibt es eigentlich immer noch SQL-Injection-Vulnerabilities? Kann es daran liegen, dass sich ein im Ansatz ziemlich bescheuertes Paradigma weitgehend ungestraft als der Standard schlechthin durchsetzen konnte?

Ich meine damit in der Tat Objekt-relationales Mapping an und für sich. Ich sehe in diesem Ansatz gut 30 Jahre nach dem Durchbruch der Objektorientierung den immer noch andauernden Kniefall vor Menschen, die ihre Weltsicht und ihre biologische Nische bislang erfolgreich gegen die Realität verteidigt haben. Die Prister des Glaubens, dass das (irrtümlich) als generell anwendbar bejubelte Muster, beliebig strukturierte Daten in Tabellenzeilen und deren Beziehungen untereinander abzubilden, der seelig machende Ansatz jeglicher Persistenz sei. Der Kniefall vor den Datenbanklern!

Aber damit nicht bescheuert genug. Als Kommunikationsprotokoll für dieses Muster des Gottstehunsbei hat sich doch wahrhaftig der unstrukturierte Freitext-String durchgesetzt. Eine der natürlichen (englischen) Sprache nachempfundene Abfrage mit gewachsener Syntax aber ohne klare technische Struktur ist das Maschine- (Applikationsserver) zu-Maschine (Datenbank)-Protokoll schlechthin geworden: SQL. Ursprünglich konzipiert als die Abfragesprache, mit der seniorige Manager, CEOs insbesondere, ihre Fragen zum Zustand des Geschäfts gleich direkt an die datenführenden Systeme schicken können sollten. Sozusagen die BI-Lösung von IBM in den 80er Jahren. Welcher verdammte Volltrottel ist eigentlich auf die Idee gekommen, den Mist maschinell zu generieren? Warum machen es alle nach? Warum ist es sogar in objektorientierten Frameworks und Standards wie Hibernate und JPA das default-Muster, Anfragen und Kommandos an das Persistenzsystem aus Textbausteinen aufzubauen? Und warum ist eine vernünftige Abfrage-API verschämt in der zweiten Reihe bzw. unter der Ladentheke zu haben? Wer kennt überhaupt z.B. die Condition-API von Hibernate, geschweige denn, nutzt sie? Und warum zum Henker ist diese mutmaßlich immernoch ein domänenspezifischer Stringbuilder, sprich, ein SQL-(fast-)Freitext-Generator?

Sagen nicht Datenbankler selbst, dass die Verwendung der Methode substring() nach todeswürdigen Vergehen wider angemessene Datenmodellierung riecht? Unter dem Gesichtspunkt schonmal den Anfrageparser eurer Lieblingsdatenbank gereviewt?

Auf dem vergeigten Ansatz blüht natürlich eine ganze Wissenschaft, die sich damit beschäftigt, wie man ihn auf eine abstraktere Art vernünftig einsetzen soll. Da gibt es die Forderung, immer denselben Abfragestring zu verwenden mit Platzhaltern, die dann von Anfrage zu Anfrage mit unterschiedlichen Variablen gebunden werden: Prepared Statements. Der Datenbanktreiber kann dann anhand des Hashcodes des Anfragestrings (…eigentlich Abfragestring-Templates) erkennen, dass es diese Anfrage schon gab und den Server über diesen Hashcode auf die für diesen Client im Server-Cache gehaltene Abfrage-Objektstruktur verweisen. Man spart sich damit die tatsächliche erneute Übertragung des Anfrage-Strings und das server-seitige Parsen. Suuuuuper!

Dass man mit syntaxbeladenem Freitext, in den syntaxfreier Freitext (vulgo Nutzdaten) eingebettet werden muss, notwendigerweise auf ein Escaping-Problem läuft, liegt auch in der Natur des Designs, nicht? Das kennen wir alle schon von, sagen wir, CSV. Mein Vorschlag wäre ja: Lasst es einfach bleiben! Aber nein, unser schöner SQL-Ansatz ist ja heilig. Aber auch hier ist man ja irgendwann mal auf die fantastische Idee gekommen, die Nutzdaten API-technisch von der Abfragesyntax zu trennen. Also so halb. Denn man kann natürlich niemandem zumuten, diesen Ansatz für verbindlich und verpflichtend zu erklären. Nein, Datenbänkler sind traditionsbewusst. Der datentragende Anfragestring war die Ursprungsidee, ist heilig und wird selbstverständlich weiter unterstützt. Prepared Statements bleiben optional, ihre Verwendung einzuklagen kann natürlich nur organisatorisch gelöst werden. Toooll!

Was soll ich denn eigentlich Entwicklern vorwerfen, die aus Unerfahrenheit in den von ihnen entwickelten Systemen SQL-Injection-Vulnerabilities eingeführt haben? Dass sie an irgendeiner Stelle eben den StringBuilder ihrer Programmierplattform statt des domänenspezifischen StringBuilders ihres Persistenzframeworks bzw. den Templating-Mechanismus ihres Datenbankproduktes genutzt haben? Dass sie dem Sprachumfang von SQL und dessen ursprünglicher und nach wie vor klaglos unterstützter Syntax, nein sogar dessem nativen Anwendungsmuster auf den Leim gegangen sind? Was für eine Art von Vorwurf ist das eigentlich?

Natürlich hätte der sicherheitsproblemverursachende Entwickler wissen können – wissen müssen! – was für Fallgruben in seinem Tätigkeitsfeld so zu beachten sind, keine Frage. Nur wage ich hier mal zu fragen: wer ist eigentlich der verantwortungslosere Tölpel: der, der in eine ungesicherte Baugrube hineinfällt, oder der, der dem Entstehen einer sinnlosen Baugrube zusieht und nicht einmal auf den Gedanken kommt, diese abzusichern?

Im Klartext: das eigentliche Problem sind ausnahmsweise mal nicht schlecht geschulte Programmierer. Das Problem ist eine schelchte Informationsarchitektur, ein verantwortungsloses Protokoll und ein kollektiver Unwille, Dinge besser zu machen, weil man sie dann anders machen müsste.

  1. Juli 2012

In der freien Wildbahn tobt ein ewiges Ringen: Heerscharen von Programmierern rudern gegen die NullpointerException. Eine der Ursachen, vielleicht die wichtigste: inkonsequentes Verhalten von Methoden.

Bevor Exception Handling in Programmiersprachen eingeführt wurde, mussten Fehlerzustände über die Rückgabewerte abgebildet werden. Da lag es bei Pointern natürlich nahe, null als magischen Wert für ein Problem zu verwenden. Dass man das so machte, weil es anders nicht ging, ist nicht Jedem bewusst, und so wird dieses Verhalten auch heute noch nach gemacht.

In Java und den meisten anderen obektorientierten Programmiersprachen gehört der exceptional flow und ein Klassenmodell von Exceptions zum Design, die Deklaration „fachlicher“ Exceptions zur Methodensignatur.

Nehmen wir also einen typischen Finder z.B. einer persistierten Entität:

DingsEntity findByID (EntityID id)

Es erscheint völlig nahe liegend, dass ein Finder mal nichts findet. Also bitte auch so designen:

DingsEntity findByID (EntityID id) throws EntityNotFoundException

Hier gehen wir davon aus, dass völlig zuverlässig sicher gestellt ist, dass id eindeutig ist, z.B. auch „in depth“ über ein dahinter stehendes Datenbankconstraint. Sonst lieber noch eine …NotUniqueException spendieren!

Das Schöne an diesem Konstrukt ist, dass man als Aurufer die Behandlung nicht vergessen kann. Der eine oder andere Tastaturanschlagsoptimierer wird dieser Einstufung als schön möglicherweise widersprechen, aber die meisten Leser dürften wissen, dass der Aufwand von Software nicht durch das Tippen von Sourcecode entsteht. In diesem Fall hilft natürlich auch eine IDE, zumindest wenn man sich helfen lässt.

Ach so, fast hätte ich vergessen, das Wichtigste zu erwähnen, weil es gar so klar ist: wenn unsere findById das verlangte Dings nicht findet, gibt es einfach null zurück.

Nein, tut es nicht, verdammt noch mal, sowas tut man überhaupt nicht, es wirft eine …NotFoundException!

Nun könnte die Implementierung unserer Methode ja mit externen APIs konfrontiert sein, von deren Wohlverhalten man nicht überzeugt ist. In diesem Fall lieber überprüfen: konservativ selbst auf null prüfen und ggf. die Exception werfen. Oder, wenn die API eigentlich schon richtig vereinbart ist, ein vorsorgliches assert (fail early!). Defensiv programmieren.

Äh, und wenn jetzt noch jemand seinen Kollegen bei folgendem Konstrukt erwischt:

DingsEntity dings ;
try {
dings = am.findById (id);
} catch (EntityNotFoundException enf) {
dings = null;
}

– der bringt den Kollegen bitte bei mir daheim zur Besprechung des Sachverhalts vorbei. Dafür hier ausnahmsweise meine Adresse: Tigergehege 1, Tierpark Hellabrunn, München.

Wenn unsere Methode ohnehin mehrere Objektinstanzen zurückgeben kann, also eine Collection oder eine deren Spezialisierungen, wirft man im Fall von 0 gefundenen Objekten natürlich keine Exception, gibt um Himmels willen auch niemals null zurück, sondern eine leere Collection! Auf ein if (retVal== null || retVal.isEmpty()) kann man dann in der gesamten Applikation getrost verzichten.

Wichtig bei Collections: „Nichts passt“ ist eine völlig andere Aussage als „konnte nicht suchen“! Damit sollte klar sein: wenn die Suche scheitert, wird eine geeignete Exception geworfen. Ob eine checked oder eine unchecked, hängt dabei vom Kontext ab: ist die Suche von fachlichen Voraussetzungen abhängig, ist es sinnvoll, zu Verletzungen der Vorbedingungen checked Exceptions zu designen. Das Scheitern einer Verbindung zur Datenbank wird man i.A. unchecked realisieren.

Berücksichtigen Sie das, wird auch für Ihre Anwender die Meldung „keine passenden Obekte oder interner Fehler“ der Vergangenheit angehören. Und der Code-Umfang um die Hälfte sinken, weil die Prüfungen auf unterschiedliche Arten, den Mangel eines Ergebnisses auszudrücken, der Vergangenheit angehören.

Wie man da hin kommt, wenn das System schon verfriemelt ist? Ganz einfach: Aufräumen, und zwar von unten her. Und bei der Gelegenheit das Wohlverhalten der niedrigen Schichten über Unit-Tests sicher stellen. Anschließend in den höheren Schichten die alten Fallunterscheidungen eliminieren: radikal sein! Nur Mut, es passiert nichts mehr, das Gespenst NullpointerException geistert ab sofort … woanders. Aber das ist eine andere Geschichte und soll ein andermal erzählt werden.

Ganze Systeme für Geschäftsanwendungen schreien förmlich aus ihren Sourcen, wie ihre Entwickler tagein tagaus gegen die NullpointerException anrudern.

Die Ursache ist, dass jedes Attribut, jeder Getter, jederzeit neben sinnvollen Resultaten auch null liefern kann – niemand weiß, wann es zuschlägt. Bei String-Attributen kommt erschwerend hinzu, dass sie leer oder null sein können, oder auch mal mit Leerzeichen gefüllt.

Erkennbar versuchen die anwendenden Programmierer von solchen, spontan null-befüllbaren Objektattributen, ohne jegliche logische Richtschnur zu erraten, was ein null, ein Leerstring, oder gerne auch mal ein 0 eines Zahlenattributes denn zu bedeuten haben könnte, und was für ein Systemverhalten dem angemessen sein kann.

Um in dieser Sisyphos-Aufgabe zu unterstützen, wurden schon ganze Heerscharen von Normalisierungs-Hilfsfunktionen geschrieben (nullToEmpty, emptyToNull, truncEmptyToNull…). Sieht man sich Aufrufhierarchien an, die über Code von verschiedenen Entwicklern oder Teams, oder auch nur desselben Entwicklers an verschiedenen Tagen, laufen, sieht man nicht selten, dass null’s zu Leerstrings, dann wieder zu null’s und vielmals hin und her konvertiert werden.

Schließlich gibt es auch Verwirrung, wie man die Abwesenheit von einem komplexen Attribut darstellen möchte. Ich habe wahrhaftig Code gesehen, in dem ein Objekt-Attribut als nicht gesetzt dargestellt wurde, in dem ein leer konstruiertes Objekt (also seinerseits mit lauter null-Attributen belegtes) gesetzt wurde.

Warum eigentlich so kompliziert?

Ich möchte an dieser Stelle dazu einladen, im Kern von Business-Anwendungen grundsätzlich nach der fachlichen Logik zu operieren (und diese auch in den Settern oder Konstruktoren durchzusetzen):

  • null sollte man genau dann verwenden, wenn ein Objekt eine Eigenschaft nicht hat, oder diese Eigenschaft nicht bekannt ist,
  • Leerstrings sollte man genau dann verwenden, wenn die Eigenschaft bekannt und definiert ist, aber eben leer.

Nehmen wir mal als weitgehend beliebiges Beispiel einen Vorgang, der einen Bearbeiter haben kann (wenn er schon bearbeitet wurde). Ein Bearbeiter ist eine Person, die wiederum einen Vornamen und einen Nachnamen haben möge. Wir könnten nun einen noch unbearbeiteten Vorgang auf folgende Arten repräsentieren:

  1. issue.setAssignee(null)
  2. issue.setAssignee(new Assignee())
  3. issue.setAssignee(new Assignee("",""))

Sorry, aber ausschließlich die erste Variante repräsentiert die korrekte Situation: der Vorgang hat keinen Bearbeiter. Der Vorgang hat insbesondere nicht

  • einen Bearbeiter ohne Vor- und Nachnamen
  • einen Bearbeiter, der auf den Namen getauft ist und aus der Familie stammt.

So, sagt nun der aufmerksame Beobachter, aber dann würde ja so etwas wie issue.getAssignee().getSurname() gerade auf eine NullpointerException laufen!?

Nun ja, richtig, und diese Aufrufkette ist ja auch fachlich falsch, wenn ein Vorgang nicht notwendigerweise einen Bearbeiter haben muss. Es erscheint durchaus angemessen, genau dies zu korrigieren, anstatt ein leer-namiges Personen-Objekt dort hinzuhängen und das Problem der armen Sau an den Kopf zu werfen, die mit den Namen von Personen umgehen muss. Diese Sau ist nämlich vor allem deswegen so arm, weil nun wirklich niemand damit rechnen muss, dass eine existierende Person keinen Namen hat (naja, vielleicht ist das gewählte Beispiel tatsächlich nicht international wasserdicht).

Woher kommt eigentlich die ganze Verwirrung? Meiner Beobachtung nach von Systemschnittstellen, an denen sich das Konzept nicht konsequent durchhalten lässt. Die prominenteste davon: die graphische Benutzerschnittstelle. Eingabefelder lassen sich typischerweise nicht auf „unbelegt“ stellen, sondern sie drücken ihr Nichtgesetztsein als Leere aus. Will der Anwender ein Attribut löschen, löscht er nicht das Eingabefeld, sondern den Text darin. Das ist unlogisch, aber so sind Anwender eben, und die Benutzeroberflächen folgen ihnen darin.

Systeme sind oft nicht besser. Da mag es z. B. welche geben, deren Schnittstellenspezifikationen die Abwesenheit von Attributen wahrhaftig durch eine Ansammlung von Leerzeichen wiederzugeben suchen. (Solche Designs stammen aus Zeiten, als man sich den zusätzlichen Speicherplatz oder Netzwerkverkehr für das Bit „ist leer“ einfach nicht leisten konnte. Heute dagegen kann man sich vor allem eines nicht leisten: verkorkste Software.)

Nunja, man kann nicht die ganze Welt allein retten, zumindest nicht mit einem einzelnen Release. Daher würde ich doch vorschlagen, die unlogische Darstellungsform genau an diesen Schnittstellen zu behandeln. Wenn es z.B. die von den Anwendern eingeklagte Konvention ist, dass das leer Lassen von Vor- und Nachname bedeuten möge, dass es den Bearbeiter nicht gibt, dann sollte man genau diese Konvention in die Darstellung der Benutzeroberfläche und Verarbeitung von Eingaben ziehen:

if (forename.isEmpty() && surname.isEmpty()) {
   issue.setAssignee (null);
} else {
...
}

Nicht schön, aber die (oft gewählte) Alternative ist, sich mit diesen Sonderregeln in der gesamten Business-Logik und auf allen Schichten der Anwendung herumzuschlagen.

Neubau versus Renovierung

  1. August 2011

Fangen wir mal mit einem einfachen Thema an. Schließlich muss ich mich auch erst warm schreiben.

Im Lebenszyklus von Applikationen kommt irgendwann mal der Zeitpunkt, an dem eine qualifizierte Mehrheit von damit Beschäftigten zu dem Schluss kommt, sie sei am Ende desselben. Oft ist das nur zu verständlich:

  • die Struktur der Software weist keinerlei Ähnlichkeit mehr mit der Struktur des zu lösenden Problems auf (dazu kommt bestimmt noch ein eigener Artikel!),
  • die technische Dokumentation wurde zuletzt zu Zeiten von Kaiser Barbarossa aktualisiert (in Form einer Delta-Doku, in der die Umstellung von Rauchzeichen auf Pergament beschrieben wurde),
  • der letzte Mitarbeiter mit Durchblick hat sich auf eine 1-Stunden-Woche mit vollem Lohnausgleich optimiert, in der er zudem von einem Geschäftsführer persönlich durchgehend gebenedeit wird,
  • das restliche Team von 59 Personen benötigt regelmäßig ein Jahr, um ein Attribut eines Geschäftsobjektes von float auf double umzustellen.

Gut – also weg damit! Was tun wir? Wir schreiben das System neu! Wir fangen ganz von Anfang an! Jetzt machen wir alles richtig! Die Geschäftsführung ist dabei! Das Team von 59 Personen ist dabei! Der letzte Mitarbeiter mit Durchblick sagt, dass er gerne dabei wäre, aber leider mit dem Altsystem schon so im Stress ist, dass er nur ab und zu mal in einem Meeting dabei sein kann und dort etwas sagen wird, wenn ihm etwas einfällt. Die Kollegen sind begeistert. Das Projekt läuft los. Das Projekt? Was ist das eigentlich genau? Wir wollen ja alles richtig machen, also auch das Projektmanagement! Wie geht das eigentlich? Na, das kann so schwer nicht sein, da malen wir mit Powerpoint doch mal ein paar Balken über eine Zeitachse und ein paar Punkte dazu, die beschriften wir – natürlich mit einem Datum – und nennen sie Meilensteine. Und in 3 Monaten sind wir fertig. Doch ja, bestimmt. Vielleicht kann der Kollege M. noch etwas mithelfen, dann müsste es eigentlich auch in 2 gehen. 4 Monate sind in’s Land gegangen. Die Euphorie ist ein wenig getrübt. Gerüchte machen die Runde, dass am neuen System gar nicht gearbeitet wird. Nein, das stimmt so nicht, kontert die Geschäftsführung, es gebe nur vorübergehend recht viel am Altsystem zu tun, dringend, deswegen ist das Neusystem etwas zurückpriorisiert. Aber nach wie vor ist das neue System die Zukunft. 1 Jahr später. Es ist schon viel passiert! Ein Praktikant hat ein paar Dialoge einer radikal überarbeiteten, futuristischen Benutzeroberfläche skizziert. Ein Team der unerschrockensten Experten hat unter Mithilfe des letzten Mitarbeiters mit Durchblick ein technisches Konzept erarbeitet, wie das Nebenläufigkeitsproblem im Sumpfadapter II durch ein sauberes technisches Design ein für alle mal gelöst werden kann! („das ist im Altsystem gar nicht realisierbar, aber jetzt haben wir endlich die Möglichkeit, das mal richtig zu durchdenken!“) Zu diesem Zeitpunkt erwächst dem Team ein Visionär: ein Artikel ist ihm erschienen, kurz bevor sein Browser abstürzte. Dort stand geschrieben: die Anforderungen müssen spezifiziert werden! Der Visionär spezifiziert die Anforderungen:

  1. Das System muss flexibel auf beliebige fachliche Anforderungen konfigurierbar sein.
  2. Diese Konfigurationsänderungen können durch einen technisch unerfahrenen Fachadministrator an der Benutzeroberfläche durchgeführt werden.
  3. Die Benutzeroberfläche muss futuristisch sein.
  4. Die Konfiguration muss als XML speicherbar sein, wobei die spitzen Klammern durch eckige ersetzt werden, um Anzeigefehler im Internet Explorer 5.5 zu vermeiden.
  5. Benutzer sind Rollen zugeordnet.
  6. Benutzer haben Rechte oder auch nicht. Manche mehr, manche weniger, das muss konfigurierbar sein. Die Rollen müssen dabei berücksichtigt werden.
  7. Fachliche Prozesse werden durchlaufen, das System unterstützt dabei.
  8. Der Sumpfadapter II darf keine Nebenläufigkeitsprobleme verursachen.
  9. Vollautomatische Selbstkonfigurierung des Systems ist erst in Realisierungsstufe 2 erforderlich, muss aber unbedingt im Design berücksichtigt werden.

An dieser Stelle ist der Visionär von seiner schöpferischen Tätigkeit ermattet. Der Praktikant korrigiert die zahlreichen Rechtschreibfehler und arbeitet den Text in das in der Firma verbindliche Dokumententemplate ein. Die Geschäftsführung ist begeistert. 15 Entwickler werden abgestellt, um das Neusystem auf Basis der Anforderungen zu entwickeln. Sie fangen schonmal mit dem Sumpfadapter II / neu an. Dabei evaluieren sie EJB 3.0, Spring, Hibernate, Visual Basic .net und No-SQL-Datenbanken. Ein Teilteam überprüft die Möglichkeit, beliebige Fachanforderungen konfigurativ abzubilden über die Programmiersprache F#, ein weiterer Programmierer prüft unter Anleitung des letzten Mitarbeiters mit Durchblick die Eignung der Programmiersprache Brainfuck 2D für denselben Zweck. Ein in der Hackordnung niedrig angesiedelter Teamleiter übernimmt mit einem kleinen Team von langweiligen Entwicklern für 3-4 Monate die Wartung des Altsystems, bis das Neusystem zur Verfügung steht. 2 Jahre später: Der in der Hackordnung niedrig angesiedelte Teamleiter samt Team hat – ungestört vom anderweitig abgelenkten letzten Mitarbeiter mit Durchblick – vor 23 Monaten das Prinzip durchgesetzt, dass alle Wartungsanforderungen an das Altsystem mit einer strukturierten Beschreibung der fachlichen Zielsetzung versehen werden müssen. Vor 22 Monaten fiel ihm auf, dass die Putzfrau vor 10 Jahren den Auftrag erhalten hatte, alle Releases des Altsystems zu testen und die Testresultate im Inneren des Putzmittelschranks aufzuhängen. Um diesem Auftrag gerecht zu werden, führte sie ein Notizbuch, in dem sie die gesamte Fachfunktionalität des Altsystems fortgeschrieben hatte. Vor 18 Monaten war ein Praktikant mit der Aufgabe fertig, die Notizen der Putzfrau in ein Fachkonzept zu konsolidieren. Vor 16 Monaten konnte der Teamleiter durchsetzen, dass die Putzfrau in seinem Team als Testmanagerin arbeitete, ein unvorsichtig am Büro vorbeilaufender Passant ohne aktuelles Arbeitsverhältnis wurde stattdessen mit der Reinigung des Büros beauftragt. Zu ungefähr demselben Zeitpunkt hatte ein Subteam von 2,5 langweiligen Entwicklern eine experimentelle Version des Altsystems fertig, aus dem 90% des existierenden Codes entfernt waren. Eine Gruppe von 30 Pilotanwendern erklärte geschlossen, keinerlei Unterschied zwischen dieser Version und der „Vollversion“ entdecken zu können. Vor 10 Monaten wurde – nach einem halben Jahr Regressionstests durch ein Team der Ex-Putzfrau – grünes Licht gegeben, die 10%-Version produktiv einzusetzen. Das Team von 2,5 langweiligen Entwicklern hatte inzwischen den Code in 3 Schichten refaktoriert. Die Präsentationsschicht erwies sich als so trivial, dass ein Student sie im Rahmen seiner Masterarbeit durch eine futuristische Web-GUI mithilfe eines etablierten Frameworks ersetzen konnte. Inzwischen sind die historischen Datenbankzugriffsobjekte durch einen O/R-Mapper ersetzt und die verwobenen Objektreferenzen in der Geschäftslogik analysiert, nach fachlichen Zuständigkeiten strukturiert und durch ein IoC-Framework ersetzt worden. Das Kernteam des Neusystems erkennt zu diesem Zeitpunkt, dass das neue Design des Sumpfadapter II leider inhärent nicht mit osteuropäischen Zeichen im anzubindenden Datensumpf zurecht kommen kann und beschließt eine komplette Neuentwicklung. Der letzte Mitarbeiter mit Durchblick zusammen mit dem Brainfuck 2D-Entwickler hat einen validierenden Parser für SOAP-Messages in Brainfuck 2D fertig gestellt. So, und was lernen wir daraus? Es ist ziemlich unsinnig, mit einer Truppe von Leuten, die nie in ihrem Leben mit Erfolg versprechenden Methoden Software entwickeln durften, ein System komplett neu aufbauen zu wollen: sie werden nicht wissen, wie es geht. Dagegen ist es durchaus möglich, mit Menschen, die unter vielen und drückenden Problemen leiden, Methoden zu erarbeiten, wie ein Problem nach dem anderen gelindert und schließlich gelöst werden kann. Und so lernen sie Punkt für Punkt kennen, wie man Software erfolgreich entwickelt.

PS: Nein, natürlich habe ich nichts gegen Brainfuck 2D. Das ist eine vollwertige Programmiersprache. Bestimmt.