Archiv der Kategorie: Tipps

Tipp des Tages: Dienste entfernen auch ohne PowerShell

Triviale Aufgaben können ihre Tücken haben. Beispiel: Wie ich entferne ich einen Systemdienst, der sich auch per WMI nicht mehr entfernen lässt, da z.B. im onStop-Event eine Exception auftritt? Ganz einfach durch Beenden der Exe-Datei. Doch welche Exe-Datei ist es genau? Eine Kombination aus Sc.exe und Taskkill.exe löst das kleine Problem.

Warum Sc.exe und nicht Get-Service? Weil Ersteres mit dem queryex-Parameter auch die Prozess-ID mit ausgibt.

sc.exe queryex VISEFService

Anschließend genügt der Aufruf von

Taskkill /pid 1234 /F

wenn 1234 die Prozess-ID ist.

Natürlich geht das alles auch per PowerShell, aber inzwischen bin ich an dem Punkt angekommen wo es in erster Linie darum geht, dass ich nicht zu viel tippen muss (man wird älter und bequemer;). Und geht es nur darum, eine bestimmte Aufgabe zu erledigen, sind die alten Kommandozeilentools immer noch eine sehr gute Wahl.

Per PowerShell sähe der Aufruf in etwa so aus:

stop-process (Get-CimInstance Win32_Service -Filter "Name='VISEFService'").ProcessId

Es ist nicht unbedingt der Umfang der Zeile, sondern eher der andere Denkansatz, an den man sich erst gewöhnen muss. Im Moment habe ich eher das Gefühl, dass wir uns wieder zur klassischen „Kommandozeilen-Denkweise“ zurückbewegen.

Tipp des Tages: Verzeichnisse aus der path-Umgebungsvariablen entfernen

Hin und wieder kann es erforderlich sein, einzelne Verzeichnisse aus der path-Umgebungsvariablen zu entfernen. Was sich zunächst schwierig anhören könnte, ist in der Praxis dann doch ein relativ einfacher Befehl.

Der folgende Befehl entfernt alle Verzeichnisse, die ein Python36 enthalten, aus der Path-Umgebungsvariablen.

$env:path = $env:path.split(";").where{$_ -notmatch "Python36"} -join ";"

Die Änderung gilt natürlich nur für die aktuelle PowerShell-Sitzung. Soll sie dauerhaft sein, muss der neue Wert der Umgebungsvariablen über die SetEnvironmentVariable()-Methode der Environment-Klasse für den aktuellen Benutzer oder systemweit gespeichert werden.

Praxistipp: Das Anmeldekonto für einen Systemdienst aktualisieren

Zu den wenigen Einstellungen, die für einen Systemdienst nicht per Set-Service vorgenommen werden können gehört das Ändern des System- oder Benutzerkontos, unter dem der Dienst ausgeführt werden soll. Diese Einstellung muss per WMI, der Klasse Win32_Service und ihrer Change()-Methode vorgenommen werden. Benutzername und Kennwort werden dabei im Klartext übergeben. Eine kleine Herausforderung besteht darin, dass für alle nicht verwendeten Parameter ein $null übergeben werden muss.

Bill Stewart hat ein schönes Skript erstellt ( Set-ServiceCredential.ps1), mit dem das Systemkonto für einen Systemdienst aktualisiert werden kann:

https://gist.github.com/Bill-Stewart/ab3a228903c5d6fb3c12dc1d92d3d1e8

Mir ist das Skript aber etwas zu umfangreich und formal. Die folgende Function erledigt die Einstellung etwas kürzer:

function Set-ServiceAccount
{
  [CmdletBinding()]
  param([String]$Servicename, [String]$Username, [String]$Password)
  Stop-Service -Name $ServiceName
  $service = Get-WmiObject Win32_Service -Filter "Name='$ServiceName'"
  $ret = $service.change($null,$null,$null,$null,$null,$null,$Username,$Password,$null,$null,$null)
  if ($ret.ReturnValue -eq 0)
  {
      Write-Verbose "Kontoinformation wurde erfolgreich aktualisiert."
      Start-Service -Name $ServiceName
  }
  else
  {
      Write-Warning "Die Kontoinformation konnte nicht aktualisiert werden - ReturnValue=$($ret.ReturnValue)"
  }
}

Ging alles gut, ist der ReturnValue = 0. Ansonsten eine Zahl, die einen Fehlercode darstellt. 22 bedeutet z.B., das das Konto nicht stimmt (in dem Skript von Bill Stewart werden alle Fehlercodes abgefragt und durch Fehlermeldungen ersetzt).

Tipp des Tages: Befehlsverlauf (History) durchsuchen mit PsReadline

Der Befehlsverlauf (History) der PowerShell-Konsole besitzt den (kleinen) Nachteil, das er nicht automatisch gespeichert wird und damit beim nächsten Start der Konsole nicht mehr vorhanden ist. Was nicht jeder PowerShell-Anwender wissen dürfte, sobald PSReadline verwendet wird, gibt es einen eigenen Verlauf, der über die Pfeiltasten abgerufen wird (nicht per Get-History). Praktisch ist, dass sich dieser Verlauf auch durchsuchen lässt.

Ex-Scripting Guy Ed Wilson hatte ja bereits schon 2014 (also vor mehr als 5 Jahren) darüber geschrieben:

Lange her, aber immer noch aktuell – der Umgang mit dem Befehlsverlauf der PowerShell-Konsole

Im Grunde läuft es auf den Tastaturshortcut [Strg]+[r] hinaus. Über ihn wird der Befehlsverlauf von PsReadline nach allen Zeilen rückwärts durchsucht, die mit der danach eingegebenen Zeichenfolge beginnen. Für jeden weiteren Treffer drückt man erneut [Strg]+[r]. Für was die Vorwärtssuche per [Strg]+[s] gut sein soll, ist mir dagegen nicht klar. Vielleicht hat jemand eine Erklärung.

Im „neuen“ PSReadline zeigt Get-PSReadlineKeyHandler die Tastaturshortcuts auch sehr übersichtlich an.

Registry-Suche per PowerShell – wieder einmal

Das Durchsuchen der Registry mit Get-ChildItem und Get-ChildItemProperty ist eigentlich nicht so schwer, wenn man sich ein paar Besonderheiten gewöhnt hat.

Die Anforderung ist schnell umschrieben. Eine Desktop-Anwendung werden sog. OCX-Controls. Das sind auf Dateien mit der Erweiterung .ocx basierende Programmkomponenten, die in der Registry enthalten sein müssen, damit sie von einer Anwendung verwendet werden können. Gesucht ist ein Befehl, der z.B. sämtliche Ocx-Controls auflistet. Eine kleine Herausforderung ergibt sich durch den Umstand, dass der Pfad der Ocx-Controls im (Default)-Eintrag eines Schlüssels abgelegt ist. Es gilt daher, diesen Eintrag anzusprechen (per „(Default)“, um es kurz zu machen;).

Der folgende Befehl listet alle Ocx-Einträge im Schlüssel Hkey_Local_Machine auf:

dir HKLM:\SOFTWARE\ -Recurse -ErrorAction Ignore |
 Get-ItemProperty -Name "(default)" -ErrorAction Ignore |
 Where-Object "(Default)" -like "*codejock*ocx" |
 Select-Object "(default)", PsPath

Etwas unschön ist natürlich der lange Pfad, der aus der PSPath-Eigenschaft resultiert. Mit einem Convert-Path lässt sich nur der relevante Teil abtrennen:

dir HKLM:\SOFTWARE\ -Recurse -ErrorAction Ignore |
 Get-ItemProperty -Name "(default)" -ErrorAction Ignore |
 Where-Object "(Default)" -like "*codejock*ocx" |
 Select-Object @{n="OcxPfad";e={$_."(default)"}},@{n="Regpfad";e={Convert-Path $_.PsPath}}

Eine naheliegende Erweiterung besteht natürlich daran, in einer weiteren Spalte anzugeben, ob die jeweilige Ocx-Datei auch existiert. Das lässt sich per Test-Path einfach feststellen.

dir HKLM:\SOFTWARE\ -Recurse -ErrorAction Ignore |
 Get-ItemProperty -Name "(default)" -ErrorAction Ignore |
 Where-Object "(Default)" -like "*codejock*ocx" |
 Select-Object @{n="Vorhanden";e={Test-Path $_.PsPath}}, @{n="OcxPfad";e={$_."(default)"}},@{n="Regpfad";e={Convert-Path $_.PsPath}}

Tipp des Tages: Die öffentliche IP-Adresse als Objekt erhalten

Auf https://techibee.com/powershell/powershell-get-public-ip-address-of-my-computer/3027 hat der Autor einen genauso praktischen wie anschaulichen Tipp veröffentlicht, den ich an dieser Stelle 1:1 wiedergebe.

Die Abfrage der öffentlichen IP-Adresse geht sehr einfach mit Hilfe eines kleinen Webservice unter der Adresse http://ipinfo.io.

Wie ruft man einen (REST-) Webservice per PowerShell auf? Natürlich per Invoke-RestMethod-Cmdlet:

Invoke-RestMethod -Uri http://ipinfo.io

Nett ist, dass der Aufruf in Bruchteilen einer Sekunde erstaunlich akkurat die Adresse des nächsten Telekom-Knotens liefert (entweder gibt es inzwischen einen in Esslingen und Umgebung oder diese Information wurde aus anderen Angaben abgeleitet). Da die Rückgabe ein Objekt ist, stehen Angaben wie Stadt und Land und natürlich die Ip als Eigenschaften zur Verfügung.

Kleine Tipps für Zwischendurch – Zeitspannen formatiert ausgeben

Genau wie für DateTime-Objekt, die bekanntlich eine Datum und eine Uhrzeit repräsentieren, gibt es auch für TimeSpan-Objekte, die eine Zeitspanne repräsentieren, eine formatierte Ausgabe. Allerdings nicht ganz so intuitiv wie es sein könnte.

Gleich vorweg: Alles ist bei docs.microsoft.com natürlich vorbildlich dokumentiert:

https://docs.microsoft.com/de-de/dotnet/standard/base-types/custom-timespan-format-strings

Aber zum einen bezieht sich die Dokumentation auf die Programmiersprache C# und zum anderen, wer liest wirklich eine solche Beschreibung?

Ein TimeSpan-Objekt entsteht z.B. immer dann, wenn zwei DateTime-Objekte voneinander subtrahiert werden. Es gibt Eigenschaften wie days, hours, minutes, totaldays, totalhours usw. Während z.B. hours für den Stundenanteil steht, steht totalhours für die Zeitspanne in Stunden. Beträgt die Zeitspanne z.B. 90 min, wären hours=1 und totalhours=1.5.

Möchte man ein TimeSpan-Objekt als Teil einer Zeichenkette ausgeben, muss man die Formatbezeichner kennen. Sie lauten h, m, s und fff (für Millisekunden). Das ist irgendwie naheliegend, etwas verzwickter wird es durch den Umstand, dass auf den ersten Formatbezeichner ein Apostroph folgen und die folgenden Formatbezeichner in Apostrophe gesetzt werden müssen (wer sich das ausgedacht hat).

Hier ein paar Beispiele in loser Reihenfolge.

$d =(Get-Date)
$da =(Get-Date) - $d
$da.ToString("m'm 's's'")
17m 10s
"{0:m'min 's'sec'}" -f $da
17min 10sec
 "{0:s':'fff''}" -f $da
10:831
"{0:m'min 's'sec 'fff'ms'}" -f $da
17min 10sec 831ms

PS; Dies ist mein letzter Blogeintrag auf absehbare Zeit. Bei Fragen rund um die PowerShell bitte einfach eine Mail, z.B. an pm ät activetraining de.

Praxistipp: SQL-Kommandos aus einer SQL Server-Trace-Datei herausziehen

Der folgende Tipp ist sehr speziell, aber enorm praktisch und ganz allgemein ein weiteres Beispiel für die Flexibilität beim Auswerten von Xml-Dateien per PowerShell.

Ausgangspunkt ist der Microsoft SQL Server und da wiederum der SQL Server Profiler, der alle SQL-Kommandos anzeigt, die von einer Anwendung an die Datenbank geschickt werden. Das Trace-Protokoll kann im XML-Format gespeichert werden.

Der folgende Befehl zieht nur die SQL-Kommandos aus der XML-Datei heraus, so dass sie in einer separaten Textdatei gespeichert werden könnten:

 

$Xml = [Xml](Get-Content $XmlPfad)

$Xml.TraceData.Events.Event | Where-Object Name -eq "SQL:BatchStarting" | 
 Where-Object { $_.Column.Name -eq "TextData" }  | 
  Select-Object -ExpandProperty Column | 
 Where-Object Name -eq "TextData" | 
 Select-Object -Expand "#text" > SqlText.txt

Auch wenn auch der SQL Server-Profiler eine ähnliche Option bietet, den reinen SQL-Text erhält man damit nicht.

Kleiner Fix für PScribo

PScribo von Ian Brighton ist ein Modul, das ich nach wie vor gerne verwende und weiterempfehle. Auch wenn der Versionsstand immer noch < 1.0 ist, scheint das Projekt noch aktiv zu sein (https://github.com/iainbrighton/PScribo/pulse).

Unter einem deutschsprachigen Windows erschienen zuletzt „jede Menge“ Fehlermeldungen. Eine Variable $Location war nicht definiert. Der Grund war, dass im Modulverzeichnis das Unterverzeichnis „de-De“ nicht vorhanden ist. Nachdem ich das Verzeichnis angelegt und die Psd1-Datei aus dem en-Us-Verzeichnis dort hinein kopiert hatte, lief es wieder durch.