Monthly Archive for September, 2008

Transactional NTFS: Ein totes Pferd?

Mit Vista bzw. dem Server 2008 ist das neue Transactional NTFS ebenfalls verfügbar. Höchste Zeit also, sich dieses Stück Technik auch mal anzusehen. Worum es sich dabei genau handelt möchte ich an dieser Stelle nicht großartig durchkauen, das kann man sich hier (Wikipedia) oder hier (Transactional NTFS Blog von Microsoft) durchlesen. Kurz gesagt, man kann damit Dateioperationen innerhalb von Transaktionen durchführen. Das ganze ist an die Transaktionen bei Datenbanksystemen angelegt; Man gruppiert also mehrere Arbeitsschritte in einer “LUW” (Logical Units of Work). Beim Abschließen einer Transaktion kann man dann die Ergebnisse entweder in den Datenspeicher übernehmen (Commit) oder verwerfen (Rollback).

Ein super Beispiel wie man das unter .NET verwendet findet man im Channel9. Hier gibt es eine komplette Solution mit verschiedenen Szenarien (TxF Only, TxF and SQL, TxF and WCF). Und auch die MSDN liefert einige Informationen zum Thema. Das hört sich ja alles schonmal ganz praktisch an.

Das KO-Kriterium für diese Technik ist allerdings folgendes:

Unsupported Scenarios
TxF does not support the following transaction scenarios:
- Transactions on network volumes, for example on file shares. TxF is not supported by the CIFS/SMB protocols.
- Transacted operations against files cached by client-side caching.

Nun, bei diesen Einschränkungen fällt mir kein praktisches Einsatzszenario für diese Technik ein… Soviel dann also zum Thema “Totes Pferd”.

Kick it on dotnet-kicks.de

SQL/SMS : Zusammenfassung von Softwareverteilungen ermitteln

Immer wieder möchte man mal eine kurze Übersicht über laufende Softwareverteilungen und deren Status haben. Wir arbeiten mit SMS, also ist das ja eigentlich kein Problem: In die SMS Datenbank eingeklinkt und beim Durchsehen der Tabellen und Views klappt man erstmal vom Stuhl. Wie soll man da was finden, wenn man nicht täglich damit zu tun hat?! Hier erstmal eine kurze Übersicht wo man die wichtigsten Sachen in den SMS Tabellen findet, diese Übersicht habe ich vor langer Zeit mal irgendwo im Netz gefunden und ein wenig erweitert:

Was Wo Spalte
Collection IDs dbo.v_FullCollectionMembership CollectionID
Computer Make dbo.v_GS_COMPUTER_SYSTEM Manufacturer0
Computer Make dbo.v_GS_COMPUTER_SYSTEM_PRODUCT Vendor0
Computer Make dbo.v_GS_BASEBOARD Manufacturer0
Computer Make dbo.v_GS_PC_BIOS Manufacturer0
Computer Model dbo.v_GS_COMPUTER_SYSTEM Model0
Computer Model dbo.v_GS_COMPUTER_SYSTEM_PRODUCT Name0
Computer Name dbo.v_R_System Netbios_Name0, Name0
Computer Name dbo.v_GS_COMPUTER_SYSTEM Name0
Computer Name dbo.v_RA_System_ResourceNames Resource_Name0
Computer Name dbo.v_GS_SYSTEM Name0
Computer Name dbo.v_GS_PROCESSOR SystemName0
Computer Name dbo.v_GS_MOTHERBOARD_DEVICE SystemName0
IP Addresses dbo.v_RA_System_IPAddresses IP_Addresses0
Last Bootup Time dbo.v_GS_OPERATING_SYSTEM LastBootupTime0
Last HW Inventory dbo.v_GS_WORKSTATION_STATUS LastHWScan
MAC Addresses dbo.v_RA_System_MACAddresses MAC_Addresses0
OS Install Date dbo.v_GS_OPERATING_SYSTEM InstallDate0
OS Name dbo.v_GS_OPERATING_SYSTEM Caption0
OS Name And Version dbo.v_R_System Operating_System_Name_and0
OS Version dbo.v_GS_OPERATING_SYSTEM Version0
OU dbo.v_RA_System_SystemOUName System_OU_Name0
Serial/Identifying Number dbo.v_GS_COMPUTER_SYSTEM_PRODUCT IdentifyingNumber0
Serial/Identifying Number dbo.v_GS_PC_BIOS SerialNumber0
Serial/Identifying Number dbo.v_GS_BASEBOARD SerialNumber0
Site Code dbo.v_RA_System_SMSAssignedSites SMS_Assigned_Sites0
User Name dbo.v_GS_COMPUTER_SYSTEM UserName0
User Name dbo.v_R_System User_Name0
User Name dbo.v_GS_SYSTEM_CONSOLE_USER SystemConsoleUser0
User Name dbo.v_GS_SYSTEM_CONSOLE_USAGE TopConsoleUser0
User Name dbo.v_LastLoggedOnUser USERID0

Zurück zur Aufgabe: Wie bekommen wir nun eine Zusammenfassung der Verteilungsstati? Nun, wir benötigen dazu drei Angaben: Die Advertisement ID, die ToDo Collection ID sowie die Installed Collection ID. Mitglieder der ToDo Collection sind alle Clients, die für die jeweilige Verteilung noch anstehen. In der Installed Collection stehen alle Rechner, bei denen die Verteilung schon durchgelaufen ist und SMS keinen Fehler festgestellt hat. Mit folgendem SQL Skript kann man dann die benötigten Zahlen ermitteln, als Collection IDs sollte man dann natürlich eigene angeben.

declare @AdvertisementId	varchar(8)
declare @TodoCollectionId	varchar(8)
declare @InstalledCollectionId	varchar(8) 

set @AdvertisementId		= ''
set @TodoCollectionId		= ''
set @InstalledCollectionId	= ''

select
	(select count(Name)
		from dbo.v_FullCollectionMembership
		where CollectionID = @InstalledCollectionId
	) 'Installed Clients',
	(select count(Name)
		from fthw9x8a.sms_rg1.dbo.v_FullCollectionMembership
		where CollectionID = @TodoCollectionId
	) 'ToDo Clients',
	(select count(Name)
		from dbo.v_fullcollectionmembership
		where collectionid in (@InstalledCollectionId,@TodoCollectionId)
	) 'Gesamtanzahl Clients',
	(select count(CAS.LastExecutionResult)
		from dbo.v_Advertisement ADV
			inner join dbo.v_ClientAdvertisementStatus CAS on ADV.AdvertisementID = CAS.AdvertisementID
		where CAS.LastExecutionResult is not null
			and collectionid in (@InstalledCollectionId,@TodoCollectionId)
	) 'Fehlerhafte Clients'

Und als Ergebnisse erhält man dann so etwas:

Installed Clients	ToDo Clients	Gesamtanzahl Clients	Fehlerhafte Clients
8783			29		8812			7

Damit kann man dann zum Beispiel weitere Berechnungen anstellen (z.B. Prozentwert der fehlerhaften Clients berechnen, oder auch ein Diagramm erstellen).

Kick it on dotnet-kicks.de

Der Tellerrand: Directory Scanner in Purebasic

Die Aufgabe ist eigentlich relativ simpel: Man schreibe einen Directory Scanner, der einfach ab einem bestimmten Verzeichnis die Namen,Pfade, Zeitstempel und Attribute aller Dateien und Subdirectories ausliest und in eine CSV Datei schreibt.

So einfach wie sich das anhört ist es zuerst auch umgesetzt: Eine rekursive Funktion dazu ist schnell geschrieben, und in der eigenen Codebase (oder auch hier bei dotnet-snippets.de) findet sich auch eine Klasse um CSV Dateien simpel zu schreiben. Tests auf der lokalen Maschine verlaufen auch erfolgreich. Alles ok.

Nun kommt aber die harte Realität: Läßt man das ganze nun mal zum Beispiel über einen PSDP (Primary Software Distribution Point) laufen, der circa 1 Million Dateien und Verzeichnisse enthält laufen wird man schnell feststellen, das die Performance der ganzen Sache mit .Net nun nicht grade doll ist.

Vor einiger Zeit hatte ich mich schon einmal mit Purebasic beschäftigt. Das ist eine prozedurale an Basic angelehnte Programmiersprache. Das interessante daran ist allerdings, das der Compiler richtig schnellen nativen 32bit Code produziert. Noch dazu ist es plattformunabhängig, also auch zum Beispiel für Linux verfügbar.
Also wird die ganze Aufgabe nocheinmal mit Purebasic implementiert. Wer sich nun gar nicht vorstellen kann wie das ganze dann aussieht, hier der Quellcode:

Structure fileinfo
  name.s
  path.s
  size.s
  created.s
  modified.s
  comment.s
EndStructure
Global NewList filelist.fileinfo()
Global scandate$
Global pumptofileparam.w

Procedure WriteList(scancomplete$)
  ResetList(filelist())
  While NextElement(filelist())
    If scancomplete$ = "True"
      If ListIndex(filelist()) = CountList(filelist())-1
        WriteString(0,filelist()\name + ";" + filelist()\path + ";" + filelist()\modified +";"+filelist()\created+";"+scandate$+";"+filelist()\size + ";" + filelist()\comment)
      Else
        WriteStringN(0,filelist()\name + ";" + filelist()\path + ";" + filelist()\modified +";"+filelist()\created+";"+scandate$+";"+filelist()\size + ";" + filelist()\comment)
      EndIf
    Else
      WriteStringN(0,filelist()\name + ";" + filelist()\path + ";" + filelist()\modified +";"+filelist()\created+";"+scandate$+";"+filelist()\size + ";" + filelist()\comment)
    EndIf

  Wend
EndProcedure

Procedure ScanDir(Directory$)
  Dir.l = ExamineDirectory(#PB_Any, Directory$, "*.*")
  If Dir
    While NextDirectoryEntry(Dir)
      name$ = DirectoryEntryName(Dir)
      If name$ <> "."
        If name$ <> ".."
          AddElement(filelist())
          filelist()\name = name$
          filelist()\path = Directory$ + "\"
          filelist()\size = Str(DirectoryEntrySize(Dir))
          filelist()\created = FormatDate("%dd.%mm.%yyyy %hh:%ii:%ss", DirectoryEntryDate(Dir,#PB_Date_Created))
          filelist()\modified = FormatDate("%dd.%mm.%yyyy %hh:%ii:%ss",DirectoryEntryDate(Dir,#PB_Date_Modified))
          filelist()\comment = Str(DirectoryEntryAttributes(Dir))
          If CountList(filelist()) = pumptofileparam
            WriteList("False")
            ClearList(filelist())
          EndIf
          If DirectoryEntryType(Dir) = #PB_DirectoryEntry_Directory
            ScanDir(Directory$ + "\" + name$)
          EndIf
        EndIf
      EndIf
    Wend
    FinishDirectory(Dir)
  EndIf
EndProcedure

OpenConsole()
If CountProgramParameters() => 2
  CreateFile(0,ProgramParameter(1))
  scandate$ = FormatDate("%dd.%mm.%yyyy %hh:%ii:%ss",Date())
  pumptofileparam = Val(ProgramParameter(2))
  If pumptofileparam = 0
    pumptofileparam = 5000
  EndIf
  ScanDir(ProgramParameter(0))
  WriteList("True")
  CloseFile(0)
Else
  PrintN("Usage:")
  PrintN("scan.exe [Share to scan] [Output csv file]")
  PrintN("")
  PrintN("Fieldterminator: ;")
  PrintN("Rowterminator: \r")
EndIf

Die Laufzeit des Scannens beträgt hier nun nur noch knapp 8 Minuten, nicht schlecht für circa 1 Million Dateien. Und es wird eine circa 150MB große CSV generiert, das man z.B. mittels Bulk Insert in eine Datenbank importierten könnte und eine History erzeugen könnte.

Fazit: Manchmal hilft ein Blick über den Tellerrand doch mal um Probleme kreativ zu lösen.

Kick it on dotnet-kicks.de

WTF Chrome und Google?!

Kann mir eigentlich jemand einen plausiblen Grund nennen, warum sich Googles neuer Browser in das Userverzeichnis ( %userprofile%\local settings\application data\google ) installiert? Und sich dann auch noch mit – bei mir – knapp 180 MB breitmacht? Ohne servergespeichertes Profil mag das ja durchaus diskutierbar sein, spätestens aber wenn man an Domänen mit zig tausend Usern denkt klappen sich doch bei den Admins die Fußnägel hoch…

Kick it on dotnet-kicks.de

Regular Expressions in SQL

Gestern hatte ich ja schon die Basics für SQL-CLR Integration beschrieben, heute treiben wir das mal weiter auf die Spitze: Wir wollen Regular Expressions im SQL Server nutzen.
Dazu erweitern wir uns ersteinmal die Klasse um drei neue Funktionen:

  • RegEx_HasMatches soll uns sagen ob überhaupt Matches auftreten
  • RegEx_MatchCount soll uns sagen wie viele Matches auftreten
  • RegEx_GetMatches soll uns die Matches aufsgeben

Für die Copy+Paster unter euch ;) :

    <SqlFunction()> _
    Public Shared Function RegEx_HasMatches(ByVal inputstring As SqlString, ByVal regexstring As SqlString) As SqlBoolean
        If Regex.Matches(inputstring.Value, regexstring.Value, RegexOptions.IgnoreCase).Count > 0 Then
            Return True
        Else
            Return False
        End If
    End Function

    <SqlFunction()> _
    Public Shared Function RegEx_MatchCount(ByVal inputstring As SqlString, ByVal regexstring As SqlString) As SqlInt64
        Return Regex.Matches(inputstring.Value, regexstring.Value, RegexOptions.IgnoreCase).Count
    End Function

    <SqlFunction()> _
    Public Shared Sub RegEx_GetMatches(ByVal inputstring As SqlString, ByVal regexstring As SqlString)
        Dim mc As MatchCollection = Regex.Matches(inputstring.Value, regexstring.Value, RegexOptions.IgnoreCase)
        Dim result As New SqlDataRecord(New SqlMetaData("Match", SqlDbType.NVarChar, 4000))
        Dim pipe As SqlPipe = SqlContext.Pipe
        pipe.SendResultsStart(result)
        For Each item As Match In mc
            result.SetString(0, item.Value)
            pipe.SendResultsRow(result)
        Next
        pipe.SendResultsEnd()
    End Sub

Nach dem Bereitstellen können wir das ganze auch schon testen:

declare @inputstring nvarchar(4000)
set @inputstring= 'My favorite web sites include:
	</P><A HREF="http://www.xentrope.de">Xylon''s Blog</A></P>
	<A HREF="http://dotnet-forum.de">.NET Forum</A></P>
	<A HREF="http://blogs.msdn.com/bclteam">.NET Base Class Library blog</A></P>'

declare @regexstring nvarchar(4000)
set @regexstring = 'href\s*=\s*(?:"(?<1>[^"]*)"|(?<1>\S+))'

select dbo.RegEx_HasMatches(@inputstring,@regexstring) 'HasMatches'
	,dbo.RegEx_MatchCount(@inputstring,@regexstring) 'Nr of Matches'
exec dbo.RegEx_GetMatches @inputstring,@regexstring

Und als Ergebnisse bekommen wir:

HasMatches	Nr of Matches
1			3

Match
HREF="http://www.xentrope.de"
HREF="http://dotnet-forum.de"
HREF="http://blogs.msdn.com/bclteam"

Coole Sache! :)

Kick it on dotnet-kicks.de

Split Funktion mittels CLR

Eine beliebte Stringfunktion fehler leider nach wie vor im SLQ Server: Split. Das hat sich auch mit dem neuen SQL Server 2008 immer noch nicht geändert. Ich hatte ja schon mal eine Splitfunktion gepostet, möchte dies aber jetzt als CLR Funktion implementieren. Lange Reder kurzer Sinn:

  • Man erstellt im Visual Studio ein neues Datenbankprojekt.
  • In den Projekteinstellungen sollte die Assembly signiert werden (Einstellungen – Signing – Sign the assembly)
  • Unter dem Karteireiter “Database” kann vorerst das Permissionlevel auf Safe bleiben, da wir ja keine externen Resources ansprechen wollen.
    Safe = Zugriff auf Daten des eigenen SQL Servers über den In-process Managed Provider
    External = Zugriff auf externe Resourcen ist erlaubt
    Unsafe = Es kann unmanaged Code ausgeführt werden
    Erhält man eine Fehlermeldung beim Bereitstellen der Assembly, kann es daran liegen das der Datenbank nicht vertraut wird. Dann sollte man die Trustworthy Eigenschaft setzen:

    Alter Database [DB] Set Trustworthy on
    
  • Man fügt dem Projekt eine neue Klasse hinzu, ich habe sie einfach “Stringfunctions” genannt.
  • Dieser Klasse fügen wir eine Prozedur hinzu:
        <SqlProcedure()> _
        Public Shared Sub Split(ByVal Input As SqlString, ByVal Delimiter As SqlString)
            Dim pipe As SqlPipe = SqlContext.Pipe
            Dim splittet() As String = Input.Value.Split(Delimiter.Value.ToCharArray)
            Dim reslist As New List(Of SqlMetaData)
            For i As Integer = 0 To splittet.Length - 1
                reslist.Add(New SqlMetaData("Col" + i.ToString(), SqlDbType.NVarChar, 4000))
            Next
            Dim result As New SqlDataRecord(reslist.ToArray())
            For i As Integer = 0 To splittet.Length - 1
                result.SetString(i, splittet(i))
            Next
            pipe.Send(result)
        End Sub
    

    Das Attribut SqlProcedure() sorgt dafür, das beim Bereitstellen automatisch eine neue Stored Procedure angelegt wird. Als Parameter der Prozedur übergeben wir zwei SQLStrings: Input und Delimiter. Wofür diese stehen sagen die Namen ja schon… ;) Um die Anzahl der notwendigen Spalten in unserer Ergnistabelle zu ermitteln legen wir uns ersteinmal eine neue Liste mit Spaltendefinitionen an. Danach wird unsere Ergebnistabelle erstellt:

    Dim result As New SqlDataRecord(reslist.ToArray())
    

    Jetzt können wir endlich alle Elemente aus dem Array in die Ergebnistabelle schreiben und zurückgeben:

    For i As Integer = 0 To splittet.Length - 1
    	result.SetString(i, splittet(i))
    Next
    pipe.Send(result)
    

    Die Verwendung der Funktion ist denkbar einfach:

    exec Split 'feld1;feld2;feld3;feld4;feld5;feld6;feld7',';'
    

    Und liefert uns eine Tabelle zurück:

     Col0	Col1	Col2	Col3	Col4	Col5	Col6
     feld1	feld2	feld3	feld4	feld5	feld6	feld7
     
Kick it on dotnet-kicks.de

SQL2005: Nützliche System Stored Procedures

SQL-Statement für jede Tabelle in der aktuellen Datenbank ausführen

exec sp_MSforeachtable '[SQL-Statement]'

SQL-Statement für jede Datenbank ausführen

exec sys.sp_MSforeachdb '[SQl-Statement]'

Tabelleninformationen auslesen (Erstellungsdatum, Schema, Tabellentyp (Table/View) etc.)

exec sys.sp_tables_rowset '[Tabellenname]'

Spalten in der Tabelle (Name, Collation)

exec sys.sp_tablecollations '[Tabellenname]'

Stored Procedures in der aktuellen Datenbank (Name, Owner, Anzahl Eingabeparameter (-1 = 0), Anzahl Ausgabeparameter (-1 = 0), Prozedurtyp)

exec sys.sp_stored_procedures
Kick it on dotnet-kicks.de