About

This page contains a single entry from the blog posted on November 29, 2007 4:57 PM.

The previous post in this blog was Abgerundete Ecken.

The next post in this blog is Ein Desktop für den Admin.

Many more can be found on the main index page or by looking through the archives.

Powered by
Movable Type 3.31

« Abgerundete Ecken | Main | Ein Desktop für den Admin »

wvk-4, consolvix+trac+svn

Hallo Welt! Um vier Weisheitszähne erleichtert und mit Kühlakkus an den Hamsterbacken ist es mir nun eine kleine Freude, ein paar Worte zu den aktuellen Errungenschaften im Rahmen von Consolvix zu verlieren. Zuerst einmal muss ich feststellen, dass die "Kern-Infrastruktur" nicht so wirklich wachsen will. Außerdem habe ich noch kein schlüssiges Konzept für eine brauchbare GUI. Zwar kann man alle in der Datenbank vorhandene Entitäten wunderbar CRUDden, aber damit ist es ja nicht getan. Ich bin jedoch zuversichtlich, dass sich das innerhalb der nächsten Wochen klären lässt.

Was hat sich also ereignet? Ich habe mich im Rahmen einer akuten Notwendigkeit zu folgendem verleiten lassen: eine einfache Weboberfläche um Subversion-Repositories zu verwalten und ihnen ein Trac-Projekt zuzuordnen sowie letztere auch detailliert konfigurieren können, zu schreiben. Das an sich ist nichtt besonders anspruchvoll, nur Auslesen einiger Shellscript-Ausgaben (svnlook z.B.), lesen einiger verzeichnisinhalte und lesen und schreiben einer Konfigurations- (INI-) Datei. In meinem judendlichen Übereifer von den eleganten Fähigkeiten des allmächtigen ActiveRecord geblendet, sollten "natürlich" SvnRepositories und TracEnvironments als solche Objekte behandelt werden, damit die Konfiguartion auch schon einfach ist. OK, zugegebenermaßen wäre das bei SvnRepository nicht nötig gewesen, aber auf die Errungenschaften bei TracEnvironment bin ich schon ein wenig stolz ;-)...

OK, was zeichnet eine TracEnvironment aus?

  • ist ein Verzeichnis, liegt zusammen mit anderen projekten in /var/trac => "Datenbank"
  • hat eine INI-Datei zur Konfiguration => Werte = Spalten
  • wird aufgesetzt durch den Aufruf von "trac-admin initenv " => "save"
  • muss nachträglich über ein Webformular mit Validierung berarbeitet werden können => klassische ActiveRecord-bezogene Aufgabe
  • beim Anlegen via "tracadmin initenv" wird _nur die Projektumgebung angelegt, nicht die Konfiguration. Also muss aus einer "sample"-Datei die konfiguration generiert werden, aber nur beim Anlegen => schöne Aufgabe für before_create
  • bis auf die initial anzugebenden Argumente (s.o.) sind alle anderen Konfigurationsdirektiven optional und theoretisch auch beliebig erweiterbar => "Spalten" müssen irgendwie dymanisch generiert werden können.

Zuerst: hier kam wieder ActiveRecord::BaseWithoutTable zum Tragen. Alle bei der Initialisierung zwingend notwendigen Spalten wurden "fest verdrahtet", der Rest wird nachher dynamisch über die Konfigurationsdatei gemacht. Alo reicht zur Datendefinition folgendes:

class TracEnvironment < ActiveRecord::BaseWithoutTable
  TRAC_BASE_PATH     = '/var/trac'
  TRAC_CONFIG_FILE   = 'conf/trac.ini'
  TRAC_TEMPLATE_PATH = '/usr/share/trac/templates'

  column :name,                   :string
  column :path,                   :string
  column :trac__database,         :string, 'sqlite:db/trac.db'
  column :trac__repository_type,  :string, 'svn'
  column :trac__repository_dir,   :string
  column :trac__templates_dir,    :string, TRAC_TEMPLATE_PATH

  validates_presence_of :name, :trac__repository_dir
  validates_inclusion_of :trac__repository_type,
                        :within => ['svn'],
                        :message => 'currently, only "svn" is supported.'
  before_create :initenv
end

die Konstanten dürften selbsterklärend sein. Doch warum diekomischen Spaltennamen mit "__"? Bei denen handelt es sich um Werte, die nachher in die Konfigurationsdatei geschrieben werden. Das Problem mit der Konfigurationsdatei ist, dass diese in "Abschnitte" unterteilt ist, also z.B. sowas:

[abschnitt1]
var1 = val1
var2 = val2
[abschnitt2]
var3 = val3
...

das ist eine zweidimensionale Struktur, die ich über die Konvention ActiveRecord-Spaltenname = "#{Abschnittsname}__{Einstellunsname}" auf das @attributes-Array des ActiveRecord-Objektes abbilde. Unter Angabe der definierten Spalten kann man eine TracEnvironment erstellen, ganz genau wie man ein ganz normales AR-objekt in der Datenbank erstellt -- mit den gleichen Methoden, callbacks etc:

...
@trac_environment = TracEnvironment.new params[:trac_environment]
@trac_environment.save
...

Handelt es ich um ein neues Objekt (@newrecord==true), wird vor dem Speichern (übe beforecreate) erst die ganze Ordnerstruktur etc. angelegt, danach die Konfiguration geschrieben:

  def update
    temp_file_name = "#{self.config_file}~"
    file = File.new(temp_file_name, 'w')
    self.config_file_struct.each do |section, value|
      file << "\n[#{section}]\n"
      value.each do |attr, attr_val|
        file << "#{attr} = #{self.send "#{section}__#{attr}"}\n"
      end
    end
    FileUtils.move temp_file_name, self.config_file
    @new_record = false
    self
  end

  def create
    command = "trac-admin #{self.path} initenv #{self.name} #{self.trac__database} #{self.trac__repository_type} #{self.trac__repository_dir} #{self.trac__templates_dir}"
    @initialisation_log = "<em>#{command}</em>\n" + `#{command}`
    case $?.exitstatus
    when 0 # everything alright
      return true
    when 1 # general failure, just clean up and return
      self.destroy
      return false
    when 2 # environment exists already
      return false
    else # unknown exit status (however, not a success): cleanup and issue an error
      raise "trac-admin initenv returned exit code #{$?.exitstatus}"
      self.destroy
    end
    self
  end

ich spare mir jetzt eine detaillierte Erklärung, fragt mich wenn etwas unklar ist. Das Lesen der Konfiguration geschieht hier:

def parse_config_file!(file=nil)
  config  = {}
  section = ''
  begin
    file = File.new(file||self.config_file)
    file.each_line do |line|
      if /^[([a-z0-9_]*)]$/.match line
        section = $1
      elsif /^\s*([a-zA-Z0-9_]*)\s*=\s*(.*)$/.match line
        self.send "#{section}__#{$1}=", $2
      end
    end
  rescue Errno::ENOENT
  end
  config
end

im Zusammenspiel mit einer überschriebenen methodmissing, die die Setter-"Methoden" für die in der Konfigurationsdatei vorhandenen Werte zu überschreiben. Das ist nötig, weil die eigentliche methodmissing eine spalte nicht anlegt, wenn diese nicht explizit definiert wurde (in diesem Fall über "column ...", ansonsten über auslesen der Datenbanktabelle). Die überschribene Methode fügt neue, unbekannte attribute einfach ins @attributes-Array ein, danach kann ActiveRecord damit arbeiten wie mit allen anderen Attributen.

def method_missing(name, *args)
  method_name = name.to_s
  if /^([a-z0-9_]+)__([a-z0-9_]+)=$/.match(method_name)
    begin
      super
    rescue NoMethodError
      key = "#{$1}__#{$2}"
      @attributes[key] = args[0]
    end
  else
    super
  end
end

Alles in allem ist das Konzept, Dateisystemartige Strukturen mit Konfigurationswerten auf ActiveRecord abzubilden, in meinen Augen ziemlich praktisch. Auf Applikationsebene arbeitet man mit genau den gleichen Strukturen wie an anderer Stelle auch, und man spart sich jede Menge Ärger mit Validierung, Flusskontrolle über Callback-Methoden, usw. Nur ein Wermutstropfen bleibt noch: so schöne Aufrufe wie

TracEnvironment.find_by_trac__repository_type "svn"

gehen NOCH nicht. Vielleicht macht es Sinn, das später einmal zu implementieren, so viel Aufwand wäre es ja nicht!

Achja, und was wäre ein so langer Post ohne ein Bild:

snap24.png

snap26.png

snap27.png

Vielen Dank für's Lesen, Ich freue mich auf Kommentare!

TrackBack

TrackBack URL for this entry:
http://www.innoq.com/movabletype/mt-tb.cgi/2918

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)