Monthly Archives: February 2012

Atomare farbige Konsolenausgabe (thread safe)

Ich hätte mir nicht gedacht, dass ich mal über so etwas triviales wie die Konsolenausgabe posten werde, aber wieso nicht, fange ich mit etwas an, was in jedem “Hello World!”-Beispiel zu finden ist und reichere es mit einem – hoffentlich – wissenswerten Tipp zum Thema Threading an… 😉

In einem meiner vielen Mockup-Projekte, in welchen ich einfach naiv drauf los programmiere um zu Lernen (jaja, so was mach ich ständig, nennt sich auch “trial and failure” oder “learning by doing”), bin ich auf folgendes Problem gestoßen:

Ich habe in meinem Programm, welches mehrere Threads instanziiert, in den einzelnen Threads Meldungen ausgegeben, was denn gerade passiert.

Nun ist das ja an sich nichts ungewöhnliches, WriteLine ist „thread safe“ und kann ohne Bedenken in mehreren parallel laufenden Threads verwendet werden, ohne dass sich die Threads in die Quere kommen.

Um in meinem Testprogramm nun Fehler besser von den „normalen“ Meldungen zu unterscheiden, habe ich diese rot eingefärbt.

Um die Konsolenausgabe einzufärben und anschließend in den Ursprungszustand wiederherzustellen ist folgender Code vonnöten:

Console.ForegroundColor = consoleColor;
Console.WriteLine("Meldung");
Console.ResetColor();

Wieso hab ich denn ein Problem damit?

Das Ganze hat nun aber einen entscheidenden Haken: der Code ist nicht atomar, das heißt, wird dieser Code von zwei Threads gleichzeitig aufgerufen, dann kann es sein, dass die Ausgabe die falsche Farbe hat!
Am besten ich erkläre es an einem Beispiel.
Folgender Code führt parallel eine Konsolenausgabe aus und weist ihr eine zufällige Farbe zu:

Random rnd = new Random();
Parallel.For(0, Console.WindowHeight - 1, i =>
{
    ConsoleColor consoleColor = (ConsoleColor)rnd.Next(7, 16);

    Console.ForegroundColor = consoleColor;
    Console.WriteLine("Meldung: {0}", consoleColor);
    Console.ResetColor();
});

Das Ergebnis kann man hier bewundern. Wie man sieht stimmen die Farben überhaupt nicht zusammen!

Die Lösung

Was man nun machen muss ist das Ganze atomar zu machen, also alle Threads während dieser Operation zu blockieren, bis die Ausgabe erledigt wurde.

Dies kann in C# mit einem Monitor.Enter oder mit dem C# Schlüsselwort lock (ist nichts anderes als ein Alias auf Monitor.Enter) erledigt werden:

lock (Console.Out)
{
    Console.ForegroundColor = consoleColor;
    Console.WriteLine("Meldung: {0}", consoleColor);
    Console.ResetColor();
}

Alles was innerhalb dieses locks passiert ist atomar, dh. keine anderen Konsolenoperationen kommen meinem Code in der Zwischenzeit in die Quere.

Advertisements
Tagged , ,

TYPO3 unter Windows mit Wincache beschleunigen

Was ist Wincache?

Wincache, oder Windows Cache Extension, ist ein PHP Optimierer, ähnlich wie APC. Er generiert aus den PHP-Quelldateien Bytecode, welcher im Speicher des Servers abgelegt wird. Somit muss dieser Bytecode nur einmal erstellt werden und nicht jedes Mal, wenn ein PHP-Script aufgerufen wird. Im Unterschied zu APC läuft Wincache nur auf Windows-Systemen, hat aber den Vorteil, dass er speziell für diese Plattform optimiert ist.

Wincache und APC bieten außerdem eine Caching API, welche es möglich macht Daten abzulegen und in darauffolgenden Requests wieder zu verwenden. Auf vielen Systemen wird dafür entweder die Datenbank (der Klassiker bei TYPO3) oder memcached verwendet.

Auch wenn die Unterschiede zwischen den beiden Bytecode-Optimierern eher marginal sind, fiel meine Wahl schließlich auf Wincache, einfach aus dem Grund, da der Webserver ein Windows 2008 Server ist und Wincache speziell für die Windows-Plattform entwickelt wurde und es laut verschiedener Benchmarks um einen Tick schneller als APC zu laufen scheint (ich muss zugeben ich habe selbst noch keinen Vergleichstest gemacht, verlasse mich aber mal auf die Angaben anderer).

Warum Wincache und TYPO3?

TYPO3 kannte lange Zeit nur zwei Caching-Methoden. Entweder das Dateisystem oder die Datenbank. Dies sind nicht gerade ideale Orte um Daten mit hohem I/O-Durchsatz abzulegen. Später wurden diese Cache „Backends“, wie sie TYPO3 nennt, konfigurierbar. Unter Anderem kamen ein memcached- und ein APC-Backend dazu.

In meinem konkreten Fall wurde auf der existierenden Webseite memcached unter Windows eingesetzt. Memcached ist wohl der Klassiker auf *nix-Systemen, wenn es um das Zwischenspeichern von Daten im Arbeitsspeicher, auch über mehrere Maschinen hinweg, geht. Es gibt auch Ports für Windows-Systeme, welche einfach einen Windows-Dienst bereitstellen, welcher memcached.exe hostet.

Habe ich schon erwähnt, dass die Seite auf welche ich mich beziehe langsam war? Nicht? Dann hole ich das jetzt nach: die Seite war träge, vor allem die Startseite! Die Startseite brauchte warm (!) – also nachdem die meisten Inhalte im Cache vorhanden waren – teilweise über eine Sekunde, bis sie sich endlich dazu entschlossen hat HTML-Code auszuspucken. Okay, es waren einige dynamische Inhalte vorhanden, wie z.B. Banner, welche zufällig ausgespuckt werden sollten, deshalb wurde auf ein aggresives Caching verzichtet. Trotzdem: es dauerte einfach viel zu lange…

Zeit mir das Ganze auf einem dedizierten System genauer anzusehen. Ich kopierte also die TYPO3-Seite und alle notwendigen Serverkomponenten auf mein Testsystem, hab alles wie auf dem Live-System konfiguriert und führte ein paar Tests durch. Mit mehr oder weniger demselben Ergebnis (sieht man von der unterschiedlichen Prozessoranzahl und verfügbarem Arbeitsspeicher ab): das Ding skalierte einfach nicht!

Ich hab mich daraufhin dazu entschlossen dem System auf den Zahn zu fühlen. Ich muss zugeben, ich habe mich mit TYPO3 seit fast zwei Jahren nicht mehr beschäftigt und das Caching war relativ neu für mich (ich kannte noch den DB-Cache). Aber nach einem Blick in die localconf.php (soweit konnte ich mich noch aus meiner früheren Zeit mit TYPO3 erinnern) ist mir der Konfigurationsabschnitt mit $TYPO3_CONF_VARS['SYS']['caching'] aufgefallen und dass es einige Einträge mit t3lib_cache_backend_MemcachedBackend gab. „Könnte es sein, dass das ein Flaschenhals ist?“ hab ich mir gedacht und mir überlegt, ob man das Ganze nicht mit Wincache beschleunigen könnte.

Dafür muss es doch eine „Backend“-Klasse geben!

Ich wurde enttäuscht. Nachdem ich einen Blick in den Ordner „t3libcachebackend“ geworfen hatte war kein Backend auffindbar, welches Wincache verwendet. Auch im Internet wurde ich nicht fündig.

Da es jedoch ein Backend für APC gibt und ich wusste, dass die beiden Byteoptimierer von ihren Features und der API fast identisch sind, habe ich einfach den bestehenden Code für Wincache portiert. Zu finden ist diese portierte Klasse am Ende des Artikels.

Was noch fehlt, ist dieses Backend in TYPO3 zu registrieren. Dies kann in der localconf.php folgendermaßen vorgenommen werden:

$TYPO3_CONF_VARS['SYS']['caching']['cacheBackends']['t3lib_cache_backend_WincacheBackend'] =
'<path_to_the_class>/class.t3lib_cache_backend_wincachebackend.php:t3lib_cache_backend_WincacheBackend';

Nun muss noch konfiguriert werden, wo das Backend verwendet werden soll. Dies kann über folgenden PHP-Code vorgenommen werden:

$TYPO3_CONF_VARS['SYS']['caching']['cacheConfigurations'] = array(
'cache_hash' => array(
'backend' => 't3lib_cache_backend_WincacheBackend',
'options' => array()
),
'cache_pages' => array(
'backend' => 't3lib_cache_backend_WincacheBackend',
'options' => array()
), 'cache_pagesection' => array(
'backend' => 't3lib_cache_backend_WincacheBackend',
'options' => array()
),
'cache_extbase_reflection' => array(
'backend' => 't3lib_cache_backend_WincacheBackend',
'options' => array()
)
);

Falls es Extensions gibt, welche ebenfalls vom Caching Gebrauch machen, müssen diese ebenfalls mit angegeben werden.

Nachdem alles registriert wurde, sollte die Seite weiterhin ohne Einschränkungen funktionieren, nur dass diesmal der TYPO3-Cache von Wincache verwaltet wird.

Hat’s denn was gebracht?

Ich muss ehrlich gestehen, ich war mir nicht sicher, ob das Ersetzen von memcached durch Wincache allzu viel bringen wird. Ich wurde aber aufs positivste überrascht! Die Seite reagierte plötzlich auf Anfragen viel schneller. Vor allem die Startseite wirkte „frischer“.

Da ich aber diesen subjektiven Eindruck durch einen objektiven Beleg untermauern wollte habe ich “ab” (Apache Benchmark Tool) angeworfen. Dieses kleine aber praktische Tool simuliert Anfragen an einen Server und spuckt anschließend eine Statistik aus, wie schnell die Anfragen abgearbeitet werden konnten.

Das Tool sollte am besten auf derselben Maschine ausgeführt werden, auf welcher die Webseite selbst läuft, damit die Statistik nicht durch Netzwerklatenzen verfälscht wird.

Und hier die Ergebnisse:

Um nun 20 Requests zu simulieren, wobei 4 Requests jeweils parallel ausgeführt werden, kann man folgende Einstellung vornehmen:

.ab.exe -n 20 -c 4 http://%5Bhostname%5D/

memcache aktiviert

25.837 seconds (1291.850 [ms]/Request)

Wincache aktiviert

9.500 seconds (475.005 [ms]/Request)

Obendrein skaliert das Ganze auch noch sehr gut, wie man sehen kann:

60 Requests, wobei 12 parallel ausgeführt werden

.ab.exe -c 12 -n 60 http://%5Bhostname%5D/

memcache aktiviert

71.586 seconds (1193.100 [ms]/Request)

Wincache aktiviert

14.645 seconds (244.083 [ms]/Request)

Zusammenfassung

Zusammenfassend kann man sagen, dass der Einsatz von Wincache wirklich einen merklichen Performanceboost bringt. Und das Ganze ist noch dazu einfach zu erreichen! Eine PHP-Datei und einige Konfigurationseinstellungen genügen, um TYPO3 um 200% zu beschleunigen!

Natürlich hängt der Geschwindigkeitszuwachs von der Komplexität der Seite und der Menge an zwischengespeicherten Daten ab und nicht immer ist ein solches Ergebnis zu erreichen, aber wenn ein solch eklatanter Boost der Lohn für die Mühen ist, sollte man es auf einen Versuch ankommen lassen! 😉

Update

Ich habe den Quellcode inzwischen ins Repository von TYPO3 committed (https://review.typo3.org/#/c/10204/) und wer weiß, mit etwas Glück schafft er es vielleicht in ein offizielles Release… 🙂

Tagged , , , , , ,

Notiz an mich: ‘throw’ ist gut, ‘throw ex’ ist böse

Richtig:

catch (FooException ex)
{
    // mach hier dein Ding
    throw;
}

Falsch:

catch (FooException ex)
{
    // mach hier dein Ding
    throw ex;
}

throw ex ist ein Fehler der von C#-Entwicklern gerne gemacht wird (vor allem von den Programmieren mit Java-Hintergrund), bzw. den man des Öfteren in Beispielen und Foren findet.

Der Unterschied ist, dass bei throw ex im Gegensatz zu throw der StackTrace verloren geht, was eigentlich NIE gewünscht ist.

Tagged , , ,

SharePoint 2010 Management Shell Ausgaben anpassen

Die “SharePoint 2010 Management Shell”, welches im Grunde eine PowerShell-Instanz ist, welche das PSSnapin mit dem Namen Microsoft.SharePoint.PowerShell geladen hat, ist extrem flexibel.

Ich könnte jetzt eine seitenweise Huldigung über die PowerShell schreiben, werde mich aber am Riemen reißen und nur einen der vielen Erweiterungspunkte der PowerShell ansprechen.

Die Erweiterungspunkte welche ich ansprechen möchte sind in der Hilfe von PowerShell dokumentiert.
Die beiden man-pages (wenn man die Linux-kompatible Bezeichnung für die Hilfe verwendet) nennen sich about_Types.ps1xml und about_Format.ps1xml:

man about_Types.ps1xml
man about_Format.ps1xml

Was mit diesen beiden XML-Dateien möglich ist, ist zum Einen das Typsystem der PowerShell zu erweitern (Types.ps1xml) und zum Anderen die Ausgabe auf dem Host zu beeinflussen (Format.ps1xml).

Types.ps1xml

Eines der wohl promintesten Beispiele für eine Erweiterung des Typsystems ist wohl das Count AliasProperty des Array-Objektes. .NET-Programmierer wissen, dass die Länge eines Arrays über das Length-Property ermittelt wird. Andere Collections benutzen dafür aber das Count-Property. Um diese Inkonsistenz zu vermeiden führt die PowerShell das Length AliasProperty ein.

Überall in der PowerShell findet man solche Typerweiterungen. Es gib verschiedenste Möglichkeiten einen Typ zu erweitern, AliasProperty ist die simpelste Möglichkeit und ist einfach ein Verweis auf ein bereits bestehendes Property (im Beispiel des Array.Count ist dies ein Verweis auf Array.Length). Es gibt aber auch komplexere, mit welchen Berechnungen möglich sind, wie z.B. das ScriptProperty. Weitere Informationen bitte about_Types.ps1xml entnehmen.

Format.ps1xml

Diese Datei ermöglicht die Ausgabe der Powershell auf dem Host zu beeinflussen. Ein einfaches Beispiel ist die Ausgabe des Verzeichnissinhaltes mit dir: da es sich im Grunde um .NET-Objekte handelt, welche zurückgegeben werden, muss der PowerShell mitgeteilt werden, wie diese schlussendlich dem Benutzer präsentiert werden sollen, da sonst jede einzelne Eigenschaft in einer Liste ausgegeben wird. Lesbarer (und auch vom DOS-Prompt her gewohnt) ist die Ausgabe mit einer eingeschränkten Anzahl von Eigenschaften und einer tabellarischen Ausgabe. Und genau hierfür sorgt die angesprochene Datei. Weitere Informationen bitte about_Format.ps1xmlentnehmen.

Und was bringt’s mir im Zusammenhang mit SharePoint?

Nun, SharePoint bietet leider eine sehr rudimentäre Implementierung dieser beiden Erweiterungspunkte. Zu finden sind die Formatdateien unter: %PROGRAMFILES%Common FilesMicrosoft SharedWeb Server Extensions14CONFIGPOWERSHELLFormat und die Typedateien unter: %PROGRAMFILES%Common FilesMicrosoft SharedWeb Server Extensions14CONFIGPOWERSHELLTypes.

Wie gesagt, diese Format- und Typdefinitionen sind recht rudimentär. Auf einem Produktivsystem sollten diese Definitionen aber ausreichen. Auf einem Entwicklersystem ist es aber manchmal bequemer, wenn man etwas mehr Informationen aus der PowerShell-Ausgabe erhält, als dies standardmäßig der Fall ist.

Weiter unten habe ich zwei Dateien angehängt, welche nach belieben angepasst werden können. Sie sollen lediglich als Ideengeber dienen.

Geladen werden die Dateien mittels den zwei Kommandos:

Update-TypeData -Prepend SharePoint.types.ps1xml
Update-FormatData -Prepend SharePoint.format.ps1xml

Diese beiden Befehle können im Profil hinterlegt werden, sodass sie beim Aufruf der PowerShell automatisch geladen werden.

Die Reihenfolge sollte wie oben sein (Type, dann Format), da die Ausgabe oftmals auf die definierten Typen verweist.

Falls nachträglich Änderungen an den beiden Dateien durchgeführt wurden, können die Definitionen über Update-TypeData und Update-FormatData neu geladen werden (ohne -Prepend).

Beispielausgabe

Standardausgabe von SharePoint:

Nach dem Laden der neuen Typ- und Ausgabedefinitionen:

Beispieldateien

Die Informationen, welche ausgegeben werden sollen, können für jeden Benutzer/Entwickler unterschiedlich sein, deshalb habe ich jeweils ein Beispiel-XML für die Formate und eines für die Types als Ausgangsdateien erstellt. Diese können dann selbst erweitert und angepasst werden:

SharePoint.format.ps1xml

<Configuration>
  <ViewDefinitions>
    <View>
      <Name>Microsoft.SharePoint.SPWeb</Name>
      <ViewSelectedBy>
        <TypeName>Microsoft.SharePoint.SPWeb</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader>
            <Width>25</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>20</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>9</Width>
          </TableColumnHeader>
          <TableColumnHeader />
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>Title</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>LastItemModifiedDate</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Language2</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Url</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
    <View>
      <Name>Microsoft.SharePoint.SPList</Name>
      <ViewSelectedBy>
        <TypeName>Microsoft.SharePoint.SPList</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader />
          <TableColumnHeader>
            <Width>20</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>10</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>9</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>36</Width>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>Title</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>LastItemModifiedDate</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>FieldCount</PropertyName>
                <Alignment>Right</Alignment>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>ItemCount</PropertyName>
                <Alignment>Right</Alignment>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>ID</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
    <View>
      <Name>Microsoft.SharePoint.SPListItem</Name>
      <ViewSelectedBy>
        <TypeName>Microsoft.SharePoint.SPListItem</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader>
            <Width>8</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>19</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>20</Width>
            <Label>ContentType</Label>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>7</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>20</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>19</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Width>19</Width>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>ID</PropertyName>
                <Alignment>Right</Alignment>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Title</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <ScriptBlock>$_.ContentType.Name</ScriptBlock>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Version</PropertyName>
                <Alignment>Right</Alignment>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Author</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Created</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Modified</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

SharePoint.types.ps1xml

<Types>
  <Type>
    <Name>Microsoft.SharePoint.SPWeb</Name>
    <Members>
      <AliasProperty>
        <Name>LCID</Name>
        <ReferencedMemberName>Language</ReferencedMemberName>
        <TypeName>System.Int32</TypeName>
      </AliasProperty>
      <ScriptProperty>
        <Name>Language2</Name>
        <GetScriptBlock>
          [System.Globalization.CultureInfo]::GetCultureInfo($this.LCID).Name
        </GetScriptBlock>
      </ScriptProperty>
    </Members>
  </Type>
  <Type>
    <Name>Microsoft.SharePoint.SPList</Name>
    <Members>
      <AliasProperty>
        <Name>Url</Name>
        <ReferencedMemberName>DefaultViewUrl</ReferencedMemberName>
        <TypeName>System.Uri</TypeName>
      </AliasProperty>
      <ScriptProperty>
        <Name>FieldCount</Name>
        <GetScriptBlock>
          $this.Fields.Count
        </GetScriptBlock>
      </ScriptProperty>
    </Members>
  </Type>
  <Type>
    <Name>Microsoft.SharePoint.SPListItem</Name>
    <Members>
      <ScriptProperty>
        <Name>Url</Name>
        <GetScriptBlock>
          $this["ServerUrl"]
        </GetScriptBlock>
      </ScriptProperty>
      <ScriptProperty>
        <Name>Author</Name>
        <GetScriptBlock>
          $this.Fields.GetFieldByInternalName("Author").GetFieldValueAsText($this["Author"])
        </GetScriptBlock>
      </ScriptProperty>
      <ScriptProperty>
        <Name>Created</Name>
        <GetScriptBlock>
          [DateTime]$this["Created"]
        </GetScriptBlock>
      </ScriptProperty>
      <ScriptProperty>
        <Name>Modified</Name>
        <GetScriptBlock>
          [DateTime]$this["Modified"]
        </GetScriptBlock>
      </ScriptProperty>
      <ScriptProperty>
        <Name>Version</Name>
        <GetScriptBlock>
          if ($this.Versions -and $this.Versions.Count -gt 0) {
          $this.Versions[0].VersionLabel
          }
        </GetScriptBlock>
      </ScriptProperty>
      <ScriptProperty>
        <Name>FieldCount</Name>
        <GetScriptBlock>
          $this.Fields.Count
        </GetScriptBlock>
      </ScriptProperty>
      <ScriptProperty>
        <Name>WorkflowCount</Name>
        <GetScriptBlock>
          if ($this.Workflows) { $this.Workflows.Count } else { 0 }
        </GetScriptBlock>
      </ScriptProperty>
      <ScriptProperty>
        <Name>RunningWorkflowCount</Name>
        <GetScriptBlock>
          if ($this.Workflows -and $this.Workflows.Count -gt 0) {
          @($this.Workflows | ? { !$_.IsCompleted }).Count
          } else { 0 }
        </GetScriptBlock>
      </ScriptProperty>
    </Members>
  </Type>
</Types>
Tagged , ,

Wie kann ich prüfen, ob ein Benutzer Mitglied einer Gruppe im Active Directory ist?

Wem es so ergangen ist wie mir und gedacht hat, ist doch ganz einfach:
Ich instanziere ein neues System.DirectoryServices.AccountManagement.UserPrincipal und prüfe mit der Methode IsMemberOf, ob der Benutzer ein Mitglied der Gruppe ist, der täuscht sich!

Was zunächst relativ trivial erscheint, entpuppt sich als relativ verzwickt, wenn man mit Untergruppen arbeitet, denn dann gibt die Methode false zurück, obwohl man Mitglied einer solchen Gruppe ist.

Und genau das ist auch das Verzwickte daran, denn es wird klappen, wenn man nach der Gruppe prüft, welche direkt dem Benutzer zugewiesen wurde, jedoch das Prüfen auf Mitgliedschaft einer “Gruppe einer Gruppe” wird ein falsches Ergebnis zurückgeben, nämlich immer false.

Anbei ein Beispiel, wie so eine Hierarchie in der Praxis aussehen könnte:

Gruppe “Alle Benutzer von Firma …”

Gruppe “Alle Benutzer der Abteilung …”

Gruppe “Alle Benutzer der Abteilung … am Standort …

Benutzer “Hans Mustermann”

Ruft man nun

string userName = "HMustermann";         // Benutzer "Hans Mustermann"
string groupName = "MusterfirmaAlle";    // Gruppe "Alle Benutzer von Firma ..."

var pc = new PrincipalContext(ContextType.Domain);
var adUser = UserPrincipal.FindByIdentity(pc, userName);
var adGroup = GroupPrincipal.FindByIdentity(pc, groupName);

bool isMember = adUser.IsMemberOf(adGroup);

auf, wird man überrascht feststellen, dass isMember false ist.

Was man nun also tun muss ist die Untergruppen durchtraversieren und jeweils überprüfen, ob der Benutzer Mitglied dieser Gruppe ist.

Dies kann mit folgendem Code einfach erreicht werden:

private static IEnumerable<GroupPrincipal> GetGroupPrincipalsRecursive(
    GroupPrincipal groupPrincipal)
{
    yield return groupPrincipal;
    foreach (var subGroup in groupPrincipal.GetMembers()
        .OfType<GroupPrincipal>())
    {
        foreach (var subSubGroup in GetGroupPrincipalsRecursive(subGroup))
            yield return subSubGroup;
    }
}

Nun kann man folgendermaßen überprüfen ob es sich um ein gültiges Mitglied handelt:

bool isMember = GetGroupPrincipalsRecursive(adGroup)
    .Any(g => adUser.IsMemberOf(g));

Extension-Method

Da ich Erweiterungsmethoden liebe, hier ein weiteres Beispiel einer Implementierung:

static class UserPrincipalExt
{
    public static bool IsMemberOfRecursive(this UserPrincipal user, GroupPrincipal group)
    {
        return GetGroupPrincipalsRecursive(group)
            .Any(g =>
            {
                try { return user.IsMemberOf(g); }
                catch (PrincipalOperationException) { return false; }
            });
    }

    private static IEnumerable<GroupPrincipal> GetGroupPrincipalsRecursive(
        GroupPrincipal groupPrincipal)
    {
        yield return groupPrincipal;
        foreach (var subGroup in groupPrincipal.GetMembers()
            .OfType<GroupPrincipal>())
        {
            foreach (var subSubGroup in GetGroupPrincipalsRecursive(subGroup))
                yield return subSubGroup;
        }
    }
}

Verwendung:

bool isMember = adUser.IsMemberOfRecursive(adGroup);
Tagged , ,

PowerShell Script zum Konvertieren von .XLS-Dateien in .XLSX-Dateien

Eines Vorweg: Ich bin kein Freund davon alle alten Excel-Dateien auf Gedeih und Verderb in das “neue” OpenXML-Format zu konvertieren. Das alte binäre Format ist in den allermeisten Fälle noch gut genug, deshalb sollte von Datei zu Datei entschieden werden, ob sich eine Konvertierung auszahlt.

Mir fallen auf die Schnelle vier Gründe ein, wofür sich eine Konvertierung lohnt:

  • Es sollen die neuen Features von Excel verwendet werden. Dies ist schlicht und einfach nur mit dem neuen Dokumentformat möglich.
  • Es muss Speicherplatz gespart werden (das neue Format ist teilweise um die Hälfte platzsparender als das alte Binärformat).
  • Ein oder mehrere Dokumente sollen automatisiert werden. Diese Aufgabe ist über das OpenXML-SDK möglich, ohne dass das Office Paket installiert ist. Dies ist vor allem auf einem Server (z.B. SharePoint, Office 365) wichtig, da dort eine Installation des Office Paketes tunlichst vermieden werden sollte.
  • Es sollen die neuen Funktionen zur Gruppenzusammenarbeit genutzt werden, sodass mehrere Benutzer auf ein und dasselbe Excel-Dokument zugreifen können und dort Änderungen vornehmen (dies setzt einen SharePoint 2010 Server oder Office 365 voraus).

Da eine Konvertierung alter Dokumente nicht immer eine Entscheidung einer einzelnen Person ist und dafür nicht immer ein Praktikant zur Verfügung steht, der einem diese Aufgabe abnimmt (alte Datei aufmachen, Datei als .XLSX oder, falls ein Macro enthalten, als .XLSM abspeichern) habe ich ein PowerShell-Script geschrieben, welches diese Aufgabe erleichtert.

Beispiel 1

Direktes Konvertieren von Bericht.xls nach Bericht.xlsx:

.Convert-Xls2Xlsx.ps1 Bericht.xls Bericht.xlsx

Beispiel 2

Konvertiert mehrere Excel-Dateien in einem Ordner mit Unterordnern:

PS> ls D:ExcelFolder -Recurse -Include *.xls |
>> .Convert-Xls2Xlsx.ps1 -AutoConvert

Beispiel 2 konvertiert – bei vorhandenem AutoConvert-Parameter und dem Vorhandensein von Makros – die Datei automatisch in ein “macro enabled” OpendXML (Endung .XLSM). Die konvertierten Excel-Dateien werden in demselben Ordner wie die alten Excel-Dateien abgespeichert. Falls dies nicht gewünscht ist, kann auch ein -DestinationFolder angegeben werden.

Falls eine Datei mit demselben Namen vorhanden ist, kann man mit dem Parameter -Force das Überschreiben der Datei erzwingen.

Der Script-Code befindet sich auf bitbucket.org.

Tagged , ,

Da ich selbst eine längere Zeit mit Typo3 gearbeitet habe und einige der Evolutionsstufen von Typo3 hautnah miterlebt habe, möchte ich gerne auf dieses Video hinweisen, welches die optischen Veränderungen der letzten 10 Jahre auf etwas mehr als eine Minute gekürzt zeigt.

Tagged ,