Nutzer mit sinatra-portier einfachst authentifizieren
Tuesday, 15. November 2016
Sinatra-portier ist ein Fork des Gems sinatra-browserid, das ich ebenfalls vorher geforkt hatte. Es funktioniert für den Entwickler noch genauso, der Unterschied ist, dass statt dem Button ein Formular erstellt wird, und dass Portier zum Bestätigen der Email benutzt wird.
Ein Sinatra-Projekt von Persona auf Portier umstellen
Sinatra-portier ist ganz offiziell als Gem registriert und kann darüber installiert werden:
gem install sinatra-portier
Im Sinatra-Projekt bindet man aber weiterhin sinatra/browserid ein:
require 'sinatra/browserid'
Dadurch kann man ohne Codeanpassung von Persona zu Portier wechseln. In den meisten Projekten muss man nur in der Gemfile gem sinatra-browserid
mit gem sinatra-portier
ersetzen und dann schauen, ob das Loginformular anstatt des Buttons im bestehenden Design ebenfalls funktioniert.
Die API
Es werden einige weniger Helferfunktionen und sinatraweit globale Variablen definiert, mit denen das Gem genutzt wird:
authorized?
True wenn der Nutzer sich mit Portier eingeloggt hat. Die Kernfunktion.
authorized_email
Die Email (als String), mit welcher der Nurzer eingeloggt ist. Kann benutzt werden um die genauen Rechte des Nutzers zu prüfen.
authorize!
Leitet zur Loginseite weiter, wenn der Nutzer nicht bereits eingeloggt ist.
render_login_button
Gibt das HTML des Loginformulars aus.
logout!
Loggt den Nutzer aus,
authorized?
ist danach false undauthorized_email
leer.
Ein Codebeispiel zeigt, wie die Funktionen benutzt werden können:
require 'sinatra/base' require 'sinatra/browserid' module MyApp < Sinatra::Base register Sinatra::BrowserID set :sessions, true get '/' if authorized? "Welcome, #{authorized_email}" else render_login_button end end get '/secure' authorize! # require a user be logged in email = authorized_email # browserid email ... end get '/logout' logout! redirect '/' end end
Nutzungspattern
Mit der obigen API gibt es mehrere Wege, wie man am besten Nutzer anmeldet und ihre Email prüft. Aber ich weiß noch, dass ich eine Weile brauchte um das für mich durchzustrukturieren, trotz des Beispiels. Nicht einfach zu prüfen, ob das eingegebene Passwort das gespeicherte ist, war ungewohnt. Aber im Grunde ist es noch einfacher: Prüfen, ob man die Emailadresse bereits kennt (=ist sie in der Datenbank?) und welche Rechte sie hat.
Ersten Nutzer zum Admin machen
Aber wo fängt man an? Bei der ersten Nutzung ist die Datenbank ja noch leer, es gibt nichts abzugleichen. Meine Projekte, die dieses Gem nutzen (ursprung, feedtragón und music-streamer) zeigen daher einen kleinen Installer, wenn die Datenbank leer ist. In diesem loggt der Nutzer sich über Portier ein, und diese erste Emailadresse wird dann als Adminadresse in der Datenbank gespeichert. Später prüft man, ob authorized_email
die gespeicherte Emailadresse ist, und kann so zu schützende Bereiche der Seite abriegeln.
Weitere Helfer erstellen
Um die Emailadresse auf Adminrechte zu prüfen definiert man am besten zwei weitere Helfer:helpers do def isAdmin? if authorized? if Database.new.getAdminMail == authorized_email return true end end return false end def protected! unless isAdmin? throw(:halt, [401, "Not authorized\n"]) end end end … get %r{/([0-9]+)/editEntry} do |id| protected! … end
Mit Nutzerliste abgleichen
Was aber, wenn man mehr als einen Nutzer haben will? Dann vergleicht man mit der Nutzerdatenbank.helpers do def isAdmin? if authorized? return Database.new.getAdminMail == authorized_email end return false end def isRegistered? if authorized? return Database.new.registered?(authorized_email) end end def protected! unless isRegistered? halt 401, erb(:login) end end def adminProtected! if (isRegistered? && isAdmin?) return true else halt 401, erb(:login) end end endRouten, die normale Nutzer aufrufen können, werden wie zuvor mit
protected!
geschützt. Hier wird nur geschaut, ob in der Datenbank der Nutzer registriert ist. Music-streamer z.B. hat eine Liste in den Einstellungen, in die der Admin neue Adressen und damit neue Nutzer hinzufügen kann. adminProtected!
hingegen prüft, ob authorized_email
die gespeicherte Emailadresse des Admins ist.
Das könnte dann mit einem kompletten Rollensystem erweitert werden, in dem der Code für jede Email prüft, welche Rolle und damit welche Rechte er hat. Und die Datenbankstruktur dafür bleibt simpel:
CREATE TABLE IF NOT EXISTS users( mail TEXT PRIMARY KEY, role TEXT );
Das schöne an dem System ist, was auch bei Persona schon hübsch war: Wir haben mit dem bisschen Code eine komplette Nutzerverwaltung in ein Sinatra-Projekt eingebaut, ohne ein einziges Passwort zu speichern oder auch nur einen Gedanken an Hashverfahren zu verschwenden.
No-Offset Pagination in ursprung
Thursday, 3. December 2015
Die schnellere Pagination ohne Offset habe ich inzwischen wirklich für ursprung umgesetzt. Das war letzten Endes einige Arbeit.
Statt einem Link wie archive/older=datum
sollte es dann doch wieder ein Link mit Seitenzahl sein, also archive/2
für Seite 2. Also brauchte das System eine Liste, um Seitenzahlen auf eine Datumsangaben zu mappen. Außerdem muss das System sich merken, welcher Eintrag bereits in der Pagination berücksichtigt ist. Denn immer dann, wenn die neu erstellt wird (bei jedem Löschen eines Eintrags) muss dies gezählt werden, statt einfach nur die Anzahl insgesamt vorhandener Einträge zu zählen.
Die Kernfunktion ist das Hinzufügen eine Seite zur Pagination und deren Anpassung:
# Add the page to the precomputed mapping of page to entry date, to enable the no offset pagination # This also has to take care of shrinking the buffer (the second archive page, n -1), so that all other archive pages remain stable def addToPagination(entry:) limit = 5 # the tag can't just be nil, because in sqlite3 INSERT OR REPLACE on shared primary keys detects ('abc', NULL) and ('abc', NULL) not as a conflict tags = entry.tags.empty? ? [self.NOTAG] : (entry.tags << self.NOTAG) tags.each do |tag| totalPages, totalEntries = self.getTotalPages(limit, tag) totalEntries += 1 # the current entry is not already counted by that function page = (totalEntries > 1 && totalEntries % limit == 1) ? totalPages + 1 : totalPages # start date of n is now entry.date @@db.execute("INSERT OR REPLACE INTO pagination(page, tag, startDate) VALUES(?, ?, ?)", page, tag, entry.date) if totalEntries > limit # now the start second archive page, the shrinking and growing buffer, has to be set as well tagSQL = tag == self.NOTAG ? "" : "AND id IN (SELECT entryId FROM tags WHERE tag = '#{SQLite3::Database.quote(tag)}')" bufferStart = @@db.execute("SELECT date FROM entries WHERE date < (SELECT startDate FROM pagination WHERE page = ? AND tag = ?) #{tagSQL} ORDER BY date DESC LIMIT ?", page, tag, limit).last['date'] @@db.execute("INSERT OR REPLACE INTO pagination(page, tag, startDate) VALUES(?, ?, ?)", page - 1, tag, bufferStart) if (totalEntries > (limit * 2)) && (totalEntries % limit == 1) # if we have more than two pages and the buffer just overgrew, we can set it back to 1 and move the full amount of entries to a stable page bufferEnd = @@db.execute("SELECT date FROM entries WHERE date < (SELECT startDate FROM pagination WHERE page = ? AND tag = ?) #{tagSQL} ORDER BY date DESC LIMIT ?", page - 1, tag, 1).last['date'] # this will never be changed again @@db.execute("INSERT OR REPLACE INTO pagination(page, tag, startDate) VALUES(?, ?, ?)", page - 2, tag, bufferEnd) end end end @@db.execute("UPDATE entries SET paginated = 1 WHERE id = ?", entry.id) end
Schließlich hat es aber durchaus funktioniert. Das Aufrufen einer Archivseite in einem vollen Blog ist schneller geworden, ohne dass sich für den Nutzer etwas geändert hätte. Es fehlt nur noch ein schlaueres Vorgehen beim Löschen eines Eintrags, dass die Pagination angepasst wird statt sie zu löschen und neu zu erstellen.
Wobei ich das Gefühl nicht loswerde, dass meine Implementation unnötig kompliziert ist.
Ruby: Mit LruRedux schnell und effizient cachen
Tuesday, 21. July 2015
Die Amazon-Api durchgängig nur 1-mal pro Sekunde anzusprechen führt natürlich dazu, dass die Anwendung nun deutlich länger braucht. Denn vorher wurden Anfragen so schnell losgeschickt wie möglich, und diese Bursts tolerierte Amazon – nur eben nicht immer und nicht zuverlässig. Daher brauchte es einen Weg, weniger Anfragen zu benutzen und so wieder akzeptable Geschwindigkeiten hinzukriegen.
Naheliegende Lösung war, Amazons Apiantworten für eine Weile zu cachen. Statt über SQLite zu funktionieren sollte dieser Cache jedoch im Speicher liegen. Genau das tut LruRedux, ist thread-safe und hat dazu einige nette Funktionen, beispielsweise ebenfalls eine Time-To-Live-Variante.
require 'lru_redux' class Amazon def initialize() begin @@cache rescue @@cache = LruRedux::TTL::ThreadSafeCache.new(200, 86400) # 24h … end end def getOffer(hardware) return @@cache.getset(hardware.type) do … self.search(hardware.type) end end end
Die getset-Methode ist besonders hilfreich. Der übergebene Block wird nur ausgeführt, wenn die id (hardware.type
hier) nicht bereits im Cache ist. Ansonsten wird der Block einmal ausgeführt, sein Ergebnis direkt gecached und zusätzlich noch returned. Ansonsten kann der Cache einfach wie ein Hash benutzt werden – allerdings wie ein Hash, der hier nur 200 Einträge fasst und sie nach 24 Stunden wieder vergisst.
LruRedux ist damit genau das, was ich hier brauchte: Ein pfeilschneller und einfach zu benutzender Cache, den ich als Klassenvariable setzen kann. Wie throttle-queue scheint er hervorragend zu funktionieren.
Ruby habe ich damals angefangen, weil ich Sinatra mochte. Inzwischen mag ich auch die Sprache selbst, aber es ist immer noch das Ökosystem drumherum, das besonders toll ist. Ich hätte sowohl cache als auch queue selbst bauen können, aber es wäre so viel mehr Arbeit und das Endergebnis schlechter gewesen. Ein spezifisches Problem durch kleine hilfreiche Gems zu erschlagen ist immer wieder eine schöne Erfahrung.
Ruby: Apiabfragen und anderes verlangsamen
Monday, 20. July 2015
Mit der throttle-queue wird ein bestimmter Codeblock nur x mal pro Sekunde ausgeführt. Das ist besonders praktisch für ausgehende Apiabfragen, wenn z.B. wie bei Amazons Produktapi regulär nur eine Anfrage pro Sekunde rausgeschickt werden darf und sonst der Zugang blockiert wird.
require 'throttle-queue' class AmazonApi def initialize() begin @@throttle # have only one throttle for all objects rescue @@throttle = ThrottleQueue.new 1 # 1 request per second … end end def search(keyword) result = "" # temporary variable because we can't return in the queue @@throttle.foreground(rand) { # random id, so new requests don't replace waiting requests result = @@api.item_search query: { … } } return result end
Alternativ könnte man den Codeblock auch im Hintergrund ausführen lassen, was je nach Anwendungsfall sehr praktisch sein kann.
Es ist ein relativ neues Gem, das erst seit November 2014 auf Github existiert. In meinen Tests funktionierte es einwandfrei und schien wie beworben thread-safe zu sein. Eine schöne Überraschung, so etwas überhaupt und dann noch ohne Abhängigkeit von eventmachine zu finden.
Sinatra: Code im Hintergrund ausführen
Friday, 17. April 2015
Alles was länger dauert, z.B. Trackbacks zu verschicken, sollte eine Webanwendung besser im Hintergrund ausführen. Sinatra hat dafür aber keinen Mechanismus parat. Ruby kann zwar Threads starten, aber ein Thread in einem Webserverthread könnte am Ende der Anfrage abgewürgt werden. Beim von mir genutzten Webserver puma würde genau das passieren.
Es gibt natürlich Lösungen, Delayed::Job zum Beispiel. Aber das kommuniziert über die Datenbank, braucht seine eigene Tabelle, und sah sehr Rails-lastig aus. Sucker Punch klang erst perfekt, weil es innerhalb des Sinatra-Prozesses läuft. Aber es braucht angepasste Worker-Objekte, und die Objektstruktur wollte ich nicht anpassen.
Meine einfache Lösung basiert auf der Idee eines Worker-Threads im Sinatra-Prozess. Nur dass statt eines einzelnen Threads ein Threadpool aus dem Gem ruby-thread bereitgehalten wird, dem der Code übergeben werden kann. Das funktioniert so:
require 'sinatra/base' require 'thread/pool' class Threadexample < Sinatra::Application class << self; attr_accessor :pool end # thread pool as class variable @pool = Thread.pool(2) get '/' DoingSomething.new() end end class DoingSomething def initialize() Threadexample::pool.process { # send the normal code to the threadpool puts "send trackback now in the background, while the pageload is already finished" } end end
Der Threadpool läuft dauerhaft im Sinatra-Prozess, beim modularen Stil gespeichert als Klassenvariable. Der Code im von Sinatra aufgerufenen Objekt wird an den Pool gesendet. Das schöne an der Lösung ist, dass der Code und die Objektstruktur nicht verändert werden müssen. Und dass die Threads im Pool nacheinander bearbeitet werden, bestehende Threads werden nicht einfach überschrieben.
Ruby Metaprogrammieren
Monday, 23. February 2015
Ich bin nicht sicher, ob Metaprogrammierung die richtige Bezeichnung ist - vor allem, weil es etwas hochtrabend klingt. Es geht um ein simples Problem: Man hat einen String und will eine Funktion oder eine Variable aufrufen, die so heißt wie der Inhalt des Strings.
Also: Ich habe "auto" in der Variable id
und will A.autoBahn
aufrufen. Das passiert in der Praxis öfter mal, wenn in bestimmten Teilen des Codes Variablen mit einer id konstruiert werden, z.B. autoBahn, fahrradBahn, eisenBahn…
class A attr_accessor :autoBahn def initialize self.autoBahn = "A44" end end a = A.new id = "auto" puts a.send(auto + "Bahn") # => A44
Ruby kennt auch eval, was sich genau so verhält wie erwartet: Es führt den Inhalt eines String als Programmcode aus. Nichts für von Nutzern wählbare Variablen, mächtiger als nur zum Variablen oder Methoden aufrufen, aber auch dafür kann man es nutzen.
eval "A.#{id}Bahn" # => A44
Variablen dynamisch zu lesen ist super. Es fehlt nur noch, sie ebenso schreiben zu können. Mit instance_variable_set
geht das auch. Ich bin anfangs nur darüber gestolpert, dass das erste Zeichen des Namens ein @ sein muss.
A.instance_variable_set('@Landstrasse', "B44")
Vielleicht ist es meistens nicht toll, so etwas benutzen zu müssen. Aber richtig eingesetzt kann es sehr viele Zeilen voller Wiederholungen auf einige wenige komprimieren.
Statistik in Ruby mit Statsample
Tuesday, 10. February 2015
Ich muss immer mal wieder Statistik anwenden, meist ohne dann wirklich Zeit dafür zu haben. R oder Julia ist mir fremd, daher griff ich meist auf handgeschriebene Bash-Skripts zurück und machte die schwierigeren Sachen, wie die Faktor- oder Signifikanzanalyse, mit externen Tools. Was eben gerade da ist.
Diesmal bin ich über statsample gestolpert. Das gem implementiert die relevanten Statistikfunktionen. Ich benutzte diesmal sowieso Ruby, da passte das gut in meine Datenverarbeitungsskripte. Und in meinen Augen hat es sich bewährt.
Zwei Beispiele: Ich habe in einer SQLite-Datenbank eine Sammlung von Kommentaren mit ihren Upvotes, und die Kommentare kann ich in zwei Gruppen unterteilen. Ich will wissen, ob die Verteilung der Upvotes zwischen den Gruppen sich signifikant unterscheidet, und wähle dafür einen t-Test. Das geht so:
require 'sqlite3' require 'statsample' include Statsample::Test db = SQLite3::Database.new "database.db" aUpvotes = db.execute("SELECT upvotes FROM comments WHERE group = 'a'";).flatten.to_scale bUpvotes = db.execute("SELECT upvotes FROM comments WHERE group = 'b'";).flatten.to_scale t_2=Statsample::Test::T::TwoSamplesIndependent.new(aUpvotes, bUpvotes) puts t_2.summary
Die Ausgabe enthält dann alle wichtigen Informationen und verrät mir, dass die zweite Stichprobe deutlich kleiner, die Varianz gleich ist und das benötigte Signifikanzniveau p = 0.5522
weit von statistischer Signifikanz weg ist.
= Two Sample T Test Mean and standard deviation +----------+--------+---------+------+ | Variable | mean | sd | n | +----------+--------+---------+------+ | Vector 1 | 9.1267 | 44.8019 | 3332 | | Vector 2 | 7.4539 | 59.6217 | 293 | +----------+--------+---------+------+ Levene test for equality of variances : F(1, 3623) = 0.4437 , p = 0.5054 T statistics +--------------------+--------+----------+----------------+ | Type | t | df | p (both tails) | +--------------------+--------+----------+----------------+ | Equal variance | 0.5945 | 3623 | 0.5522 | | Non equal variance | 0.4687 | 321.6479 | 0.6396 | +--------------------+--------+----------+----------------+ Effect size +-------+--------+ | x1-x2 | 1.6727 | | d | 0.2467 | +-------+--------+
Das zweite Beispiel ist ein chi²-Test um zwischen zwei Stichproben mit Kategorien zu unterscheiden. Ich habe bei der ersten Stichprobe 60 Leute in Kategorie A, 10 in B, 30 in C. Bei der zweiten sind es 60 in A, 25 in B und 15 in C. Signifikanter Unterschied?
m=Matrix[[60, 10, 30], [60, 25, 15]] x_2=Statsample::Test.chi_square(m) puts x_2.probability # => 0.003…
Hier gibt es keine schöne Ausgabe, sondern nur den p-Wert. Der ist mit 0.003 unterhalb einiger typischer Signifikanzniveaus, also ist der Unterschied zwischen den Beobachtungen für p < 0.01
beispielsweise signifikant.
Ich habe ein bisschen das Drumrum erklärt weil dort das Problem des Gems ist: Es fehlt Dokumentation. Klar, es ist nicht die direkte Aufgabe des Gems, dem Nutzer einen Statistikkurs zu geben. Es ist ein Werkzeug, mit dem Leute, die wissen was sie machen müssen, diese Aufgabe durchführen können. Aber selbst wenn man eine vage Ahnung hat findet man zu wenig darüber, wie man die Funktionen aufzurufen hat.
Performance-Tuning einer Webapp mit Index und Arbeitsvermeidung
Monday, 2. February 2015
Meinen Feedreader feedtragón nutze ich praktisch jeden Tag. Ich bin soweit zufrieden, aber nach der Eingewöhnungsphase fiel mir auf, dass er etwas langsam war. Gemessen und uff: Die längste Wartezeit kam durchs Warten auf eine Antwort vom Server. Kein gutes Zeichen. In diesem Artikel werde ich beschreiben, wie ich das Problem anging und dabei diese Verbesserung produzierte:
Die Wartezeit wurde reduziert auf ein 84stel, von 2,8 Sekunden am Anfang auf ~33ms.
Schritt 1: Index setzen
Jedes mal, wenn eine Seite geladen wird, holt feedtragón eine Liste aller abonnierten Feeds vom Server, und prüft dann jeden Feed auf ungelesene Einträge. Dafür muss er die Einträge aus der Datenbank holen, das macht er mit diesem Query:
SELECT url, title, content, id FROM entries WHERE feed = ? AND read = 0 AND id > ? LIMIT 10;
Er beschränkt sich also auf 10 Einträge mit einer höheren ID als die Start-ID, so funktioniert das Endless-Scrolling, aber das ist hier nicht weiter wichtig. Wichtig ist der Rest des WHERE-Statemends: feed = ? AND read = 0
. Da werden also zwei Spalten abgefragt. Und insgesamt dauerte das alles 2,8 Sekunden - also nicht nur dieser Query, sondern alle zusammen.
Ich bin kein Datenbankexperte, aber ich vermutete, dass diese Abfrage schneller funktionieren könnte. Das ist eine SQLite-Datenbank, dort kann man das mit EXPLAIN QUERY PLAN
prüfen:
sqlite> EXPLAIN QUERY PLAN SELECT url, title, content, id FROM entries WHERE feed = ? AND read = 0 AND id > ? LIMIT 10; 0|0|0|SEARCH TABLE entries USING INTEGER PRIMARY KEY (rowid>?)
Was heißt das jetzt? Ich musste es nachgucken. Dass er hier nur mit dem Primary Key sucht heißt, dass er für read und feed keine andere Möglichkeit hat als manuell zu suchen. Also muss dafür je ein Index her:
sqlite> CREATE INDEX entries_read_idx ON entries(read); sqlite> CREATE INDEX entries_feed_idx ON entries(feed); sqlite> EXPLAIN QUERY PLAN SELECT url, title, content, id FROM entries WHERE feed = ? AND read = 0 AND id > ? LIMIT 10; 0|0|0|SEARCH TABLE entries USING INDEX entries_feed_idx (feed=? AND rowid>?)
feedtragón gestartet, den Browser aufgemacht, in die Netzwerkanalyse geschaut und mich gefreut: Die Wartezeit auf den Server schrumpfte von 2,8s auf 0,5s. Damit war die Sache für mich erstmal abgehakt und feedtragón ausreichend optimiert.
Schritt 2: Join (Arbeitsvermeidung)
Später kam mir dann der Gedanke, dass es besser gehen müsste. Denn das Vorgehen, das ich oben beschrieb - alle Feeds holen, ans Template schicken, dort deren Einträge holen, dann nur die Feeds mit ungelesenen Einträgen anzeigen - ist keineswegs effizient. Es folgt aus einer strikten Einhaltung der für dsnblog skizzierten Architekturprinzipen: Objekte ohne Controller, die für ihre Daten zuständig sind. Das ist toll zu schreiben, besonders das Datenbanklayer bleibt einfach beherrschbar, aber Dee erwähnte damals das möglicherweise kostspielige Befüllen der Daten. Und die Einträge des Feeds aus der Datenbank zu holen ist kostspielig.
Vorher holte er also alle Feeds aus der Datenbank:
SELECT url, id, name FROM feeds;
und filterte dann im Template die Feeds aus, die keine neuen Einträge haben:
feeds.each do |feed| erb :feedlink, :locals => {:feed => feed, :current_feed_id => current_feed_id} if ! feed.entries.nil? && feed.entries.size > 0 end
wobei feed.entries() wieder die Datenbank aufruft, mit dem im ersten Schritt optimierten Query.
Stattdessen könnte man auch direkt nur die Feeds übergeben, die ungelesen Einträge haben. Dafür muss nur die Feed- und die Eintragstabelle gejoint werden:
SELECT DISTINCT feeds.url, feeds.id, feeds.name FROM feeds JOIN entries ON (entries.feed = feeds.id) WHERE entries.read = 0;
Das Filtern kann dann entfallen. Und so schrumpften die 0,5s noch einmal, auf diesmal 0,033s.
Das ist vielleicht etwas spezifisch. Der Kern hier ist: Unnötige Datenbankabfragen durch einen geschickten Join zu vermeiden ist eine gute Idee, und es auch wert wenn es etwas gegen die vorhandene Struktur geht.
Schlusswort
Natürlich sind das keine konstanten Werte. Wären mehr Feeds und mehr Einträge in der Datenbank bräuchte alles länger. Der Server hat wahrscheinlich eine langsamere Platte als mein Rechner mit seiner SSD. Außerdem ist nun nur die Wartezeit auf den Server minimiert, er muss immer noch danach etwaige Bilder herunterladen, außerdem kommt die Transportzeit zum Server dazu.
Trotzdem: Der Unterschied ist auch auf dem Server da. Statt "Klick, Warten, Rendern, da" ist es nun "Klick, Rendern, da" - den Schritt auf realistisch gesehen etwa unter eine Sekunde Ladezeit, von vorher über 3, spürt man deutlich.
Websockets mit Ruby, Sinatra und Puma
Friday, 24. October 2014
Anlass war natürlich mein momentanes Projekt, ich brauchte einen Weg, vom Server aus den Browser über Updates der Feeds zu benachrichtigen. Dafür soll der Browser nicht alle paar Sekunden beim Server anfragen, der Server soll von sich aus den Browser benachrichtigen. Und genau das geht mit Websockets.
Auf Javascriptseite wird ein neuer Websocket erstellt, dann kann über seine Funktionen auf die Nachrichten vom Server reagiert werden:
var socket = new WebSocket('ws://' + location.host + '/updated' ); socket.onopen = function() { socket.send('erste nachricht an den server'); } socket.onmessage = function(msg){ console.log(msg); }
Ein reales Beispiel findet sich hier.
Auf der Serverseite läuft bei mir Ruby mit Sinatra, und der Rack-Server ist puma. Ich habe nach einer Kollision damit in einem anderen Projekt gerne etwas Distanz zu EventMachine. Und es gibt auch ein passendes Gem, mit dem selbst bei diesem etwas seltenem Setup Websockets schnell funktionieren: sinatra-hijacker. Mein Beispielcode:
require 'sinatra' require 'sinatra/hijacker' register Sinatra::Hijacker websockets = [] websocket '/updated' do websockets << ws ws.onmessage do |msg| puts msg end ws.onclose do websockets.delete(ws) end "Done" end
Ein reales Beispiel findet sich hier.
Funktionierte am Ende erstaunlich gut. Ich muss mich allerdings noch an dieses neue Werkzeug gewöhnen. So übertrage ich bis jetzt damit nur Benachrichtigungen und hole die neuen Elemente dann per Ajax, das ist eigentlich nicht nötig, es ginge sicher auch direkt. Und ich brauchte ein bisschen um zu realisieren, dass es nicht mehr als ein Websocket pro Verbindung braucht, wenn man verschiedene Events per JSON unterscheidbar macht.
Bugfixen mit Final Term
Wednesday, 9. July 2014
Vor einiger Zeit ging Final Term durch die Newsseiten. Ein moderner Terminalermulator, der mit modernen Features und Aussehen punkten will, aber noch Alpha-Software ist. Durch Zufall an das Projekt erinnert, prüfte ich vor drei Tagen den aktuellen Stand.
Neu ist, dass sie auf Bountysource Preise fürs Bugfixen verteilen. Sehr sympathisch, der Hauptentwickler verzichtet auf Spenden, das soll nur neuen Entwicklern zugute kommen, die das Projekt brauche. Eine der Bugs ist mir ins Auge gefallen: Segfaults, GTK errors and hangs with NestingContainer. Absurde Abstürze, damit habe ich seit Simdock ein bisschen Erfahrung, daher schien mir der Versuch eines Bugfixes wie ein guter Weg, etwas neues zu lernen.
Tatsächlich ist Final Term derzeit komplett instabil. Einen Tab zu schließen führt zu Absturz oder Freeze. Interessant, denn der in Vala geschriebene Code ist nicht wirklich hässlich, höchstens fehlen ein paar Kommentare und ausgerechnet der NestingContainer ist teilweise eher verkompliziert als elegant.
Etwas Debugging später hatte ich eine Absturzursache gefixt: Das Programm sendet ein kill-Signal an den pty-Prozess und wartete danach nicht. Ich bin mir nicht völlig sicher, warum das zu Problemen führt, ich hatte nur eine Ahnung. Tatsächlich hilft ein Timeout, seitdem können zumindest auf meinem System Tabs ohne Abstürze geschlossen werden.
Freudig sendete ich den Pull-Request, nur um darauf hingewiesen zu werden, dass dies noch lange nicht alles sei: Ein Strg+D führe regelmäßig ebenfalls zu Abstürzen und Freezes, sogar noch häufiger. Und das stimmt, auch bei mir, auch mit meinem Fix. Eine Menge Debugging später fand ich heraus, wo genau er einfriert: Beim Entfernen des Tabs aus dem GTK-Notebook per
notebook.remove_page(notebook.page_num(child));
Er scheint dort aus irgendeinem Grund in eine Race-Condition(?) zu laufen und hängt sich in einem Mutex-Lock fest:
#0 0xb7fdd424 in __kernel_vsyscall () #1 0xb7012d4b in pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/i386/i686/../i486/pthread_cond_wait.S:187 #2 0xb72648ec in __pthread_cond_wait (cond=0x80db7d0, mutex=0x80da6fc) at forward.c:149 #3 0xb6a70607 in ?? () from /usr/lib/i386-linux-gnu/libxcb.so.1 #4 0xb6a70a88 in ?? () from /usr/lib/i386-linux-gnu/libxcb.so.1 #5 0xb6a70b2a in xcb_writev () from /usr/lib/i386-linux-gnu/libxcb.so.1 #6 0xb6f0a130 in _XSend () from /usr/lib/i386-linux-gnu/libX11.so.6 #7 0xb6f0a70a in _XReply () from /usr/lib/i386-linux-gnu/libX11.so.6 #8 0xb6f05f5b in XSync () from /usr/lib/i386-linux-gnu/libX11.so.6 #9 0xb774df26 in ?? () from /usr/lib/i386-linux-gnu/libgdk-3.so.0 #10 0xb772a7ae in gdk_display_sync () from /usr/lib/i386-linux-gnu/libgdk-3.so.0
Hat jemand den Fehler schonmal gesehen? Ich versuche mich derzeit an Workarounds.
Ein Wort hier zu Vala: Das erscheint mir ziemlich kompliziert. Es ist nett, objektorientiert schreiben zu können, aber wenn am Ende doch auf Speicherverwaltung geschaut werden muss und die zusätzliche Abstraktion vom C-Code eine Hürde beim Bugfixen ist, erscheint mir das ziemlich sinnlos. Ich sehe nicht, wo finalterm einen Fehler gemacht hat, es sollte durch die Verwendung moderner Frameworks nicht so instabil sein, wie es derzeit ist. Vielleicht war GTK/Clutter/Mx/Vala einfach die falsche Wahl.
Finalterm selbst macht einen guten Eindruck. Frühe Alpha-Software zwar, und ich weiß nicht, ob der Ansatz mit seinen Animationen und Menüs etwas für mich ist. Aber ich würde darauf wetten, dass es einen bleibenden Platz in der Linuxwelt bekommen wird, falls es bald genug etwas stabiler und kompletter wird.
Edit: Inzwischen hab ich den Bug meiner Meinung nach gelöst.
Simdock: xcb und wnck statt Xlib
Thursday, 15. May 2014
Simdock ist nicht perfekt. Ich mag immer noch, dass die Transparenz auf meinem System funktioniert und wie es die Fenster einzeln durchgeht. Aber eines der Probleme ist, dass es gelegentlich abstürzt(e), ohne jegliche Fehlermeldung. Ich habe daher nicht herausgefunden, woran das liegt. Aber ich hatte ein bisschen Code in einer xstuff.cc, den ich weder verstand noch schön fand. Dieser nutzte die Xlib - und dafür gibt es mit xcb eine Alternative, die bei Fehlern etwas seltener das Programm mitreißen sollte.
Warum überhaupt eine X-Library?
Berechtigte Frage. Simdock nutzt eigentlich wxWidgets, eine C++-Multipalttform-Library. Für die eigentliche Fenstererstellung funktioniert das auch ganz gut. Aber ein Dock muss Sachen machen, die andere Programme nicht tun müssen, die näher an der Aufgabe eines Fenstermanagers sind - beispielsweise Fenster anderer Programme aktivieren. Das kann wxWidgets nicht, oder zumindest ist es nicht so dokumentiert, dass ich es finden würde. Die xstuff.cc hatte diese Funktionen (und ein paar Helfer):
void xstuff_raiseWindow unsigned int xstuff_getWindowPID bool xstuff_resizeScreen void xstuff_set_wmspec_strut bool xstuff_setDock
Vorgehen
Man findet zu xcb viel weniger Dokumentation als ich gedacht habe, insbesondere viel weniger Codebeispiele. Was existiert ist aber wenigstens meist hilfreich. So auch MixingCalls, der Hinweis, dass man bestehenden Xlib-Code einfach mit xcb mischen kann. Genau das machte ich beim Ersetzen der xstuff_setDock
, die _NET_WM_WINDOW_TYPE
auf _NET_WM_WINDOW_TYPE_DOCK
setzen soll. Vorher sah die so aus:
/* Stolen from gnome-panel */ bool xstuff_setDock (Display * xdisplay, Window winID) { if (winID == 0) return false; if (!xdisplay) return false; Atom a[2] = { None, None }; a[0] = XInternAtom (xdisplay, "_NET_WM_WINDOW_TYPE_DOCK", FALSE); Atom a2 = XInternAtom (xdisplay, "_NET_WM_WINDOW_TYPE", FALSE); XChangeProperty (xdisplay, winID, a2, XA_ATOM, 32, PropModeReplace, (unsigned char *) a, 1); return true; }
Und danach so:
bool xstuff_setDock (Display * xdisplay, Window winID) { if (winID == 0 || !xdisplay) { return false; } xcb_connection_t *c = XGetXCBConnection(xdisplay); xcb_ewmh_connection_t EWMH; xcb_intern_atom_cookie_t *EWMHCookie = xcb_ewmh_init_atoms(c, &EWMH); if (! xcb_ewmh_init_atoms_replies(&EWMH, EWMHCookie, NULL)) { return false; } xcb_change_property(c, /* Connection to the X server */ XCB_PROP_MODE_REPLACE, /* Property mode */ winID, /* Window */ EWMH._NET_WM_WINDOW_TYPE, /* Property to change */ XCB_ATOM_ATOM, /* Type of the property */ 32, /* Format of the property (8, 16, 32) */ 1, /* Length of the data parameter */ &(EWMH._NET_WM_WINDOW_TYPE_DOCK)); /* Data */ return true; }
Es gibt hier drei Besonderheiten:
- Mit
XGetXCBConnection(xdisplay);
wird Display* von Xlib in xcb als connection benutzt. - xcb reichte nicht aus, weil das
_NET_WM_WINDOW_TYPE
nicht kennt. Man kann aber XInternAtom von Xlib benutzen. Ich habe stattdessen auf xcb_ewmh zurückgegriffen. xcb_ewmh_init_atoms_replies
ist von http://stackoverflow.com/a/13319374/2508518
Weitere Vereinfachungen
Im aktuellen Master ist die Funktion gleich verschwunden. Denn statt xcb gibt es noch eine Alternative: libwnck. Diese Lib nutzt simdock sowieso schon, und mit ihr geht das ganze so:
WnckWindow* window = wnck_window_get(winID); wnck_window_set_window_type(window, WNCK_WINDOW_DOCK);
Weil das ausreichend kurz ist, habe ich mehrere solcher Aufrufe in einer xstuff_setDefaultWindowFlags
kombiniert.
Doch auch für xcb gibt es noch Vereinfachungen, und zwar in xcb_ewmh. Dort sind einige Hilfsfunktionen definiert, mit denen Attribute leichter abgefragt und gesetzt werden können. Statt also mit xcb_get_property - ähnlich aufgebaut wie xcb_change_property - zu arbeiten, kann simdock die PID eines Fensters so erfragen:
unsigned int xstuff_getWindowPID(Window winID) { xcb_connection_t* conn = xcb_connect (NULL, NULL); xcb_ewmh_connection_t ewmh_conn; xcb_intern_atom_cookie_t* ewmh_cookie = xcb_ewmh_init_atoms(conn, &ewmh_conn); if(! xcb_ewmh_init_atoms_replies(&ewmh_conn, ewmh_cookie, NULL)) { return false; } uint32_t pid; xcb_ewmh_get_wm_pid_reply(&ewmh_conn, xcb_ewmh_get_wm_pid(&ewmh_conn, winID), &pid, NULL); xcb_disconnect(conn); return pid; }
Mit ein bisschen besserer Struktur (die connection müsste ja nicht in der Funktion erstellt werden) könnte das fast so simpel sein wie der Wnck-Weg.
Aktueller Stand
So bin ich Funktion für Funktion durch den Code gegangen und habe die Xlib durch xcb oder wnck ersetzt. Hat es was gebracht? Mir zumindest ist simdock bisher nicht mehr abgestürzt, aber das heißt natürlich noch nichts. Es wird sich mit der Zeit zeigen. Aber selbst wenn nun alles gut ist: Ich habe auch ein paar andere Ecken leicht verändert, z.B. die Größenberechnung von Icons und Programm, was auch geholfen haben könnte.
Aber ich glaube, dass schon die Codeersparnis bei weiteren Verbesserungen helfen wird. Und xcb bietet nette Möglichkeiten, Calls zum Xserver zu kombinieren, damit kann man sicher noch einiges verbessern.
pkg-config statt autoconf: Viel weniger Code
Wednesday, 7. May 2014
Als ich damals simdock übernommen hatte, konnte ich es zwar erweitern, Bugs fixen und das Ergebnis kompilieren, aber ich bin nie hinter das Buildsystem gestiegen. Ich bin ja kein C++-Programmierer, diese Welt war und ist mir fremd. Entsprechend wirr war das git-Repository… Ich habe durchaus versucht, das aus dem svn übernommene Chaos aufzuräumen, aber alle Dateien erschienen nötig, und da ich nach einem besonders beherzten Aufräumversuch nicht mehr Kompilieren konnte, machte ich ihn rückgängig und lies bis jetzt davon die Finger. Warum auch an einem funktionierendem System rumdoktorn, nur für die Ästhetik?
Jetzt wurde das dringend, denn unter Ubuntu 14.04 funktioniert die Kompilierung nicht mehr. Und um dort den Fehler zu finden, muss ich das Buildsystems verstehen und die Abhängigkeiten kennen. Daher habe ich das Makefile von izulu (bzw password-store) genommen und versucht, auf dessen Grundlage - ohne autoconf, ohne die herumfliegenden Installationsskripte, ohne Chaos - die Kompilierung neu einzurichten.
Hilfreich war da, dass ich inzwischen den Kompilierungsbefehl herausgefunden hatte. Doch den konnte ich ja nicht einfach ins Makefile schreiben, auf einem anderen System - 64-bit z.B. - würden die Pfade nicht passen. Das fehlende Werkzeug dafür war pkg-config, das mit pkg-config --libs --cflags Library
die Pfade und Flags für g++ erzeugen kann. Für wx kann wx-config das gleiche. Und kombiniert sieht simdocks Makefile nun so aus:
PREFIX ?= /usr DESTDIR ?= BINDIR ?= $(PREFIX)/bin APPDIR ?= $(PREFIX)/share/simdock ICONDIR ?= $(PREFIX)/share/pixmaps SRCDIR ?= src/ SHELL ?= /bin/sh CC ?= g++ CCFLAGS ?= -O2 DEPS = $(shell pkg-config --libs --cflags glib-2.0 gconf-2.0 gtk+-2.0 libwnck-1.0) DEPS += $(shell wx-config --cflags --libs) .PHONY: install uninstall all: $(CC) $(CCFLAGS) $(SRCDIR)*.cc $(DEPS) $(LDLIBS) -o simdock install: @install -d "$(DESTDIR)$(BINDIR)" "$(DESTDIR)$(APPDIR)" "$(DESTDIR)$(ICONDIR)" @install -m 0755 -v simdock "$(DESTDIR)$(BINDIR)/simdock" @install -m 0644 -v gfx/* "$(DESTDIR)$(APPDIR)/" @install -m 0644 -v gfx/simdock.png "$(DESTDIR)$(ICONDIR)/" uninstall: @rm -vf "$(DESTDIR)$(BINDIR)/simdock" "$(DESTDIR)$(APPDIR)/*" "$(DESTDIR)$(ICONDIR)/simdock.png"
Und das ist alles, ein configure gibt es nicht.
Das war eine größere und zeitraubende Aktion. Insgesamt - wobei viele der Dateien da nie reingehört hätten - konnte ich 70k Zeilen aus dem Repo entfernen, wodurch es nun von Github erstmals als C++-Projekt erkannt wird.
Hat jemand Verbesserungsvorschläge?
Firefox Erweiterungen
Thursday, 6. March 2014
Ich habe heute meine erste Firefox-Erweiterung (noch nicht reviewed) hochgeladen und ich bin ein bisschen beeindruckt davon, wie einfach das war. Natürlich habe ich das Addon-SDK benutzt, wahrscheinlich wäre es sonst auch ein bisschen schwieriger gewesen.
Man lädt also das SDK herunter und führt das bin/activate-Skript aus, um es zu laden. Dann einen Ordner erstellen, ein
cfx init
ausführen und schon ist das Grundgerüst da. Die lib/main.js ist der Startpunkt des Codes, und so einfach zeichnet man mit ihr ein Widget:
var widgets = require("sdk/widget"); var tabs = require("sdk/tabs"); var widget = widgets.Widget({ id: "mozilla-link", label: "Mozilla website", contentURL: "http://www.mozilla.org/favicon.ico", onClick: function() { tabs.open("http://www.mozilla.org/"); } });
Für mich war relevanter, wie einfach mit JS die Zielseite manipuliert werden kann:
var data = require("sdk/self").data; var pageMod = require("sdk/page-mod"); pageMod.PageMod({ include: "*.ycombinator.com", contentScriptFile: [data.url("jquery-2.1.0.min.js"), data.url("hn-colors.js")]
Wobei data/hn-colors.js den seitenspezifischen jQuery-Code enthält, der einfach runtergeschrieben werden kann.
Ziemlich simpel, und - ich hab mal einen Blick in die API geworfen - dabei trotzdem recht mächtig. Und ich fühl mich jetzt, als hätte ich eine tolle neue Fähigkeit erlernt.
Mit HTML5 Eingaben verbieten
Sunday, 19. January 2014
Ein Formular hat ein input-Eingabefeld (type text), und eine Reihe von Wörtern sind invalid, weil Kategorien mit diesem Namen schon existieren. Wie warnt man nun am besten den Nutzer, wenn seine Eingabe nicht gültig ist, statt es nach Absenden des Formulars festzustellen?
Eine Möglichkeit ist das pattern-Attribut mit Negation. Normalerweise gibt das vor, was gültig ist, also z.B. nur Wörter mit vier Buchstaben. Aber mit diesem Code lassen sich ganze Worte ausschließen:
pattern="^(?!(wort1|wort2)$).*"
rsa.sh
Wednesday, 8. January 2014
Vor ein paar Jahren versuchte man mir zu erklären, wie RSA funktioniert. Als Übung - und mit der üblichen Warnung, unseren Code nie zu benutzen - sollten wir es selbst implementieren, denn nur dann wird es verstanden. Machte ich mit Freude, natürlich in Bash, einfach weil ich es konnte. Und ich noch heute grinsen muss beim Gedanken an die Schimpfworte, die der Tutor bei der Bewertung benutzt haben müsste (wenn ich mich richtig erinnere, bekam ich sogar die volle Punktzahl - was mich dann doch überraschte).
War mir sicher, das hier verbloggt zu haben, konnte es aber (anlässlich noqqes Implementierung) nicht mehr finden. Daher, uneditiert aus der Abgabemail (oder hier als gist):
#!/bin/bash getPrim() { local range="$1" local prim=$(getRandom $range) until [[ $prim -gt 1 ]] && isPrim $prim;do prim=$(getRandom $range) done echo $prim return 0 } #Miller-Rabin-Test isPrim() { local prim=$1 if [[ $(echo "$prim % 2" | bc) -eq 0 ]];then return 1 fi #won't find an "a" for them: if [[ $prim -eq 3 ]] || [[ $prim -eq 5 ]];then return 0 fi prim_range=${#prim} local i=0 while [[ $i -lt 10 ]];do local a=$(getRandom $(($RANDOM % (prim_range + 1) )) ) until [[ $(echo "$a < ($prim - 2) && $a > 2" | bc) -eq 1 ]];do a=$(getRandom $(($RANDOM % (prim_range + 1) )) ) done if isWitness $prim $a;then return 1 fi let i++ done return 0 } function isWitness() { prim=$1 a=$2 prim_minus_one=$(echo "$prim - 1" | bc) test=1 local i=${#prim_minus_one} i=$(($i - 1)) while [[ $i -ge 0 ]];do x=$test test=$(echo "$x^2 % $prim" | bc) if [[ $test -eq 1 ]] && [[ $x -ne 1 ]] && [[ $x -ne $prim_minus_one ]];then return 0 fi let i-- done test=$(powmod $a $prim_minus_one $prim) if [[ $test -ne 1 ]];then return 0 else return 1 fi } setBasics() { local range="$1" local try="$2" if [[ -z "$range" ]];then #the length of sqrt(m) for p and q fits to the necessary length of n range=$(echo "sqrt($m)" | bc) range=${#range} fi if [[ -z "$try" ]];then try=1 fi doBasics $range $try #bash's comparison won't work with big numbers until [[ $(echo "$n > $m" | bc) -eq 1 ]];do let try++ if [[ $try -gt $(($range * 10)) ]] || [[ ${#n} -lt $(( ${#m} -2 )) ]];then let range++ try=1 fi doBasics $range $try done } function doBasics() { local range="$1" local try="$2" #two primenumbers p=$(getPrim $range) q=$(getPrim $range) until [[ p -ne q ]];do p=$(getPrim $range) q=$(getPrim $range) done #RSA-Modul n=$(echo "$p*$q" | bc -l) #eulersche phi=$(echo "($p-1)*($q-1)" | bc -l) } #choose e coprime to phi getPublicKey() { local e=$(($RANDOM)) until [[ $(echo "$e < $phi" | bc) -eq 1 ]];do e=$(($RANDOM)) done local i=2 while [[ $(echo "$i < $e" | bc) -eq 1 ]];do if [[ $(echo "$phi % $i" | bc ) -eq 0 ]] && [[ $(echo "$e % $i" | bc) -eq 0 ]];then getPublicKey exit fi let i++ done echo $e } getPrivateKey() { local result=($(extended_euclid $e $phi)) local d=${result[0]} until [[ $d -gt 0 ]];do d=$(echo "$d+$phi" | bc -l) done echo $d } function extended_euclid() { a=$1 b=$2 local x=0 local lastx=1 local y=1 local lasty=0 while [[ $b -ne 0 ]];do local quotient=$(echo "$a / $b" | bc) local temp=$b b=$(echo "$a % $b" | bc) a=$temp temp=$x x=$(echo "$lastx - ($quotient * $x)" | bc) lastx=$temp temp=$y y=$(echo "$lasty - ($quotient * $y)" | bc) lasty=$temp done local result=($lastx $lasty $a) echo "${result[*]}"; return 0 } #a^b%m: Square & Multiply powmod() { local a=$1 local b=$2 local mod=$3 local i=0 local res=1 #b in binary for binary exponentiation b=$(echo "ibase=10;obase=2; $b" | bc) while [[ $i -lt ${#b} ]];do res=$(echo "$res^2 * $a ^ ${b:$i:1} % $mod" | bc) let i++ done echo $res } getRandom() { local range="$1" if [[ -z "$range" ]];then range=$RANDOM fi local r=$((RANDOM % 10)) while [[ $r -eq 0 ]];do r=$((RANDOM % 10)) done local i=1 while [[ $i -lt $range ]];do local temp=$((RANDOM % 10)) r=${r}${temp} let i++ done echo $r } encrypt() { local m="$1" local c=$(powmod $m $e $n) echo "$c" } decrypt() { local c="$1" local m=$(powmod $c $d $n) echo "$m" } export BC_LINE_LENGTH=0 m=91011121314151617181920212223242526272829 old_m=$m setBasics e=$(getPublicKey) echo "Public Key: ($e, $n)" d=$(getPrivateKey) echo "Private Key: ($d, $n)" echo c=$(encrypt "$m") echo "c: $c" m=$(decrypt "$c") echo "m: $m" if [[ $m -ne $old_m ]];then echo "Error: Wrong message decrypted:" >&2 echo "p: $p" >&2 echo "q: $q" >&2 echo "Public Key: ($e, $n)" >&2 echo "Private Key: ($d, $n)" >&2 echo "c: $c" >&2 echo "m: $m" >&2 fi #Ausgabe #onli@Fallout:~$ uni/ts/rsa.sh #p: 783057321236353042573 #q: 444786834379004491147 #Public Key: (2969, 348293587050020677086428785700425092601231) #Private Key: (137252777651911145904238835165856311899289, 348293587050020677086428785700425092601231) # #c: 134886886292723664083288725067434182648518 #m: 91011121314151617181920212223242526272829