Willkommen zu unserer neuen PowerShell-Serie, die sich mit PowerShell-Tricks und deren Integration in PowerShell Universal beschäftigt. So funktioniert diese Serie:
- Der erste Teil jedes Artikels befasst sich mit grundlegenden PowerShell-Techniken und stellt Ihnen überraschende Tricks und Code-Beispiele vor, mit denen Sie Ihre PowerShell-Kenntnisse vertiefen können.
- Im zweiten Teil nutzen wir die kostenlose Version von PowerShell Universal, um Ihr persönliches PowerShell-Dashboard zu erstellen, damit Sie Ihren PowerShell-Code ganz einfach nutzen können, ohne Skripte zu suchen und Konsolen ausführen zu müssen.
PowerShell Universal (PSU)
Wenn Sie PowerShell Universal noch nicht kennen, ist dies der ideale Einstieg. PowerShell Universal bindet PowerShell-Code in einen Webserver ein, und mit nur wenigen Klicks können Sie damit beginnen, Ihre eigenen GUI-Dashboards zu erstellen. Es ergänzt Ihre vorhandenen PowerShell-Kenntnisse perfekt und funktioniert plattformübergreifend, sowohl mit Windows PowerShell als auch mit PowerShell 7. Damit erstellen Sie Dashboards und Tools ohne komplexe WPF-, WinForms- oder andere GUI-Techniken.
Skalierung von PowerShell-Funktionen
Schauen wir uns zunächst etwas PowerShell-Code an und wenden wir einige Tricks und Techniken an, um ihn zu optimieren.
Wollten Sie schon einmal herausfinden, welcher Prozess einen bestimmten Systemdienst hostet, nur um dann herauszufinden, dass sie in „Process Monitor“ oder „Process Explorer“ alle als „svchost.exe“ angezeigt werden?
Die Lösung umfasst zwei PowerShell-Funktionen, die unter anderem die IDs von Dienstprozessen und die Namen von Dienstgruppen abrufen. Sie funktionieren hervorragend, sind jedoch bei großem Umfang langsam. Sehen Sie selbst:
function Get-ServiceProcessId
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string] $Name
)
process
{
$svc = Get-CimInstance Win32_Service -Filter "Name='$Name'"
if ($svc -and ($svc.ProcessId -ne 0))
{
$svc.ProcessId
}
}
}
Diese tolle Funktion nimmt einen Prozessnamen entgegen und gibt dessen Prozess-ID zurück (sofern verfügbar):
PS C:\> Get-ServiceProcessId -Name spooler
4784
Da diese Funktion pipeline-fähig ist, lässt sie sich auf viele Prozesse skalieren:
PS C:\> Get-Service | Get-ServiceProcessId
5524
1960
(...)
In diesem Aufruf listet Get-Service alle Dienste auf und Get-ServiceProcessId verknüpft die Eigenschaft „Name“ automatisch mit seinem Parameter „Name“ (ValueFromPipelineByPropertyName). Da der Funktionscode in einen process {}-Block eingebettet ist, wird dieser Code für jedes Pipeline-Element wiederholt. Das Schreiben solcher pipeline-fähigen Funktionen ist wirklich nicht schwierig: Weisen Sie einem Parameter ValueFromPipeline oder ValueFromPipelineByPropertyName zu und fügen Sie Ihren Code in process {} ein – fertig. Ihre Funktion skaliert nun automatisch.
Allerdings ist der Aufruf quälend langsam: Die Prozess-IDs treffen einzeln mit sekundenlanger Verzögerung ein. Das liegt daran, dass der Funktionscode für jeden einzelnen Dienst WMI abfragt, und diese WMI-Abfrage benötigt Zeit.
Leistungsoptimierung
Das soeben beschriebene Leistungsproblem tritt häufig in Skripten auf. Sobald Informationen abgefragt werden, sei es von WMI, Active Directory, Datenbanken oder anderen Quellen, skaliert der Code nicht optimal.
Eine einfache Lösung für solche Szenarien ist die Verwendung von Nachschlagtabellen. PowerShell bietet bereits ein Cmdlet dafür. Sehen Sie selbst:
function Get-ServiceProcessId
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string] $Name
)
begin
{
$hash = Get-CimInstance Win32_Service -ErrorAction Ignore |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
$id = $hash.$Name.ProcessId
if ($id) { $id }
}
}
Diese Funktion ist blitzschnell: Bei Abfragen mehrerer Dienste wird die WMI-Abfrage nur einmal ausgeführt. Nach der anfänglichen Verzögerung durch die Abfrage werden die angeforderten Prozess-IDs ohne weitere Verzögerung ausgegeben.
Das liegt daran, dass wir der Funktion einen begin {}-Block hinzugefügt haben. Dieser Teil wird nur einmal zur Vorbereitung ausgeführt, bevor irgendetwas anderes passiert. In diesem Block fragen wir WMI nach allen Diensten ab, was nicht mehr Zeit in Anspruch nimmt als die Abfrage eines einzelnen Dienstes.
Um später Informationen zu einem bestimmten Dienst zu finden, werden die Abfrageergebnisse an Group-Object übergeben. Dieses Cmdlet eignet sich hervorragend zum Erstellen von Nachschlagtabellen, da es die Parameter -AsHashtable und -AsString unterstützt, die automatisch eine Hash-Tabelle erstellen, die anhand der von Ihnen angegebenen Eigenschaften indiziert ist. Klingt kompliziert? Ist es nicht. Schauen Sie selbst:
PS C:\> $hashtable = Get-CimInstance Win32_Service | Group-Object -Property Name -AsHashTable -AsString
PS C:\> $hashtable.Spooler.processId
4784
PS C:\> $hashtable.WSearch.ProcessId
10152
PS C:\> $hashtable.WSearch
ProcessId Name StartMode State Status ExitCode
--------- ---- --------- ----- ------ --------
10152 WSearch Auto Running OK 0
In der von Group-Object generierten Hash-Tabelle entsprechen die Schlüsselnamen nun den Dienstnamen. Wenn Sie einen bestimmten Dienstnamen angeben, erhalten Sie das dazugehörige Dienstobjekt. Wenn Sie eine bestimmte Eigenschaft hinzufügen, z. B. .ProcessId, erhalten Sie direkt die zugehörige Prozess-ID.
Lassen Sie uns zur Verdeutlichung ein weiteres Beispiel betrachten, bevor wir fortfahren:
PS C:\> $hashtable = Get-Service | Group-Object -Property Status -AsHashTable -AsString
PS C:\> $hashtable.Running.Count
156
PS C:\> $hashtable.Stopped.Count
170
PS C:\> $hashtable.Running
Status Name DisplayName
------ ---- -----------
Running AdobeARMservice Adobe Acrobat Update Service
Running AppIDSvc Application Identity
Running Appinfo Application Information
(...)
Dieses Mal haben wir nach der Eigenschaft „Status“ gruppiert und können nun jede der Statuskonstanten verwenden, um die Hash-Tabelle abzufragen. Da es mehrere Dienste mit dem Status „Running“ oder „Stopped“ gibt, liefert jeder Schlüssel der Hash-Tabelle nun mehrere Dienste. Es ist jetzt ganz einfach, die Anzahl der laufenden oder angehaltenen Dienste zu zählen oder alle laufenden Dienste auszugeben. Das Konzept der Gruppierung mit Hash-Tabellen ist sehr flexibel und äußerst leistungsstark.
Wenn man darüber jedoch einen Moment nachdenkt, wirft dies eine weitere Herausforderung auf: Nun, da Sie Ihre Funktion skalieren und die Prozess-IDs für viele Dienste schnell abrufen können, wie nützlich ist eine Liste von Zahlen, bei der der ursprüngliche Name des Dienstes, zu dem eine bestimmte Prozess-ID gehört, verloren geht?
Hinzufügen von Informationen
Um Informationen hinzuzufügen – anstatt sie zu ersetzen – verfügt PowerShell über ein weiteres Cmdlet genau für diesen Zweck. Diese verbesserte Version kann die Prozess-ID mithilfe ihres neuen Parameters -PassThru anfügen:
function Get-ServiceProcessId {
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]
[string]
$Name,
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]
[System.ServiceProcess.ServiceController]
$Service,
[switch]
$PassThru
)
begin
{
$hash = Get-CimInstance Win32_Service -ErrorAction Ignore |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
# since the user now has the option to either specify the service name
# or pipe in an actual service, let's look up the pendant so we always
# have both and can simplify the remaining code:
if ($PSCmdlet.ParameterSetName -eq 'ServiceName')
{
$Service = Get-Service -Name $Name -ErrorAction Ignore
}
else
{
$Name = $Service.Name
}
# make sure "0" values are replaced by NULL
$id = $hash.$Name.ProcessId | Where-Object { $_ -gt 0 }
if ($PassThru)
{
# take the service and add the process ID
$Service |
Add-Member -MemberType NoteProperty -Name ProcessId -Value $id -PassThru
}
else
{
$id
}
}
}
Den Parameter -PassThru finden Sie bei vielen PowerShell-Cmdlets. Und wenn er vorhanden ist, können Sie entweder einen neu berechneten Wert ausgeben oder diesen an die ursprüngliche Eingabe anhängen. Dies wird am besten anhand der Ergebnisse deutlich. Standardmäßig ändert sich nichts und Sie erhalten weiterhin die Dienstprozess-IDs:
PS C:\> Get-Service | Get-ServiceProcessId
5524
1960
(...)
Wenn Sie -PassThru angeben, erhalten Sie die ursprünglichen Eingabeobjekte (die Dienste, die Sie weitergeleitet haben):
PS C:\> Get-Service | Get-ServiceProcessId -PassThru
Status Name DisplayName
------ ---- -----------
Stopped AarSvc_78962bb Agent Activation Runtime_78962bb
Running AdobeARMservice Adobe Acrobat Update Service
Stopped ADPSvc Aggregated Data Platform Service
(...)
Allerdings verfügen diese Objekte nun über eine neue Eigenschaft namens ProcessId, die Sie zum Sortieren verwenden können. Um sie anzuzeigen, verwenden Sie Select-Object:
PS C:\> Get-Service | Get-ServiceProcessId -PassThru | Sort-Object ProcessId -Descending | Select-Object -Property DisplayName, ProcessId
DisplayName ProcessId
----------- ---------
Web Threat Defense Service 19084
Diagnostic System Host 18580
Benutzerdienst für die Plattform für verbundene Ge... 17608
WebClient 16616
Quality Windows Audio Video Experience 15328
(...)
Und so funktioniert es:
Damit eine Funktion Informationen weitergeben kann, benötigt sie Zugriff auf das ursprüngliche Eingabeobjekt. Deshalb hat die neue Funktion nun zwei sich gegenseitig ausschließende Parameter: -Name (geben Sie einen Dienstnamen wie „Spooler“ an) und -Service (geben Sie eine konkrete Dienstinstanz an). Intern erkennt die Funktion, welche Informationen der Nutzer angegeben hat und berechnet die fehlenden.
Wenn der Nutzer -PassThru angibt, wird das Dienstobjekt an Add-Member weitergeleitet. Mit Add-Member können dynamisch neue Eigenschaften zu jedem beliebigen Objekt hinzugefügt werden. In diesem Beispiel wird eine „NoteProperty“ (ein statischer Wert) namens ProcessId hinzugefügt und die neu abgerufene Prozess-ID in dieser Eigenschaft gespeichert. Das ist alles.
Sobald Sie diese Technik beherrschen, können Sie Ihren Code deutlich modularer gestalten, wie wir im Folgenden sehen werden.
Flexibles, Lego-ähnliches System
Zum Üben fügen wir zwei weitere Funktionen hinzu, die den Prozessnamen zu einer gegebenen Prozess-ID und den Namen der Dienstgruppe ermitteln. Es handelt sich dabei um denselben grundlegenden Ansatz mit denselben Komponenten, die wir gerade besprochen haben.
Hinzufügen eines Prozessnamens
Hier ist eine modulare PowerShell-Funktion, die den Klartext-Prozessnamen von jedem Objekt abruft, das eine Eigenschaft namens ProcessId oder Id vom Typ Integer besitzt:
function Get-ProcessNameById {
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[Object]
$InputObject,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[int]
[Alias('Id')]
$ProcessId,
[switch]
$PassThru
)
process
{
$ProcessName = if ($ProcessId)
{
(Get-Process -Id $ProcessId).Name
}
if ($PassThru)
{
$InputObject |
Add-Member -MemberType NoteProperty -Name ProcessName -Value $ProcessName -PassThru
}
else
{
$ProcessName
}
}
}
Werfen wir einen kurzen Blick auf die hier verwendeten PowerShell-Programmiertricks:
- Der
param()-Block referenziert das Eingabeobjekt zweimal: Es ist unverändert in$InputObjectvorhanden (um es bei Bedarf später weiterzugeben) und seine Prozess-ID ist in$ProcessIdvorhanden. PowerShell übernimmt die komplexe Aufgabe und identifiziert die passende Eigenschaft. Sie müssen lediglich den gewünschten Typ (in diesem Beispiel[int]) und den gültigen Namen (ProcessId,Id) angeben.[Alias()]akzeptiert eine durch Kommas getrennte Liste, sodass Sie beliebig viele Eigenschaftsnamen angeben können. Jeder dieser Namen ist für diesen Parameter zulässig, und der erste wird verwendet. - Wenn im Eingabeobjekt eine Prozess-ID gefunden wurde, befindet sich diese in
$ProcessId. Der Code verwendet eine einfacheIf-Anweisung, um den Wert in den Prozessnamen umzuwandeln. Ist$ProcessIdjedoch NULL (weil die Eigenschaft im Eingabeobjekt fehlte oder leer war), ist auch$ProcessNameNULL. Der Trick besteht hier darin,$ProcessNamederIf-Anweisung zuzuweisen und nicht innerhalb derIf-Klausel, sodass der Wert auf NULL gesetzt wird, wenn dieIf-Bedingung nicht erfüllt ist. Sehen Sie, wie elegant und vielseitig die neue Funktion eingesetzt werden kann. Sie können sie mit jedem Eingabeobjekt verwenden, das eineProcessId- oderId-Eigenschaft besitzt. Ermitteln Sie zum Beispiel den Namen Ihres aktuellen PowerShell-Hosts:
PS C:\> Get-Process -id $pid | Get-ProcessNameById
powershell_ise
Oder wenden Sie sie auf unsere Dienste an und erhalten Sie eine Liste der Dienste, ihrer zugrundeliegenden Prozesse und Prozess-IDs:
PS C:\> Get-Service | Get-ServiceProcessId -PassThru | Get-ProcessNameById -PassThru | Where-Object ProcessId | Select-Object -Property Name, ProcessName, ProcessId | select -First 5
Name ProcessName ProcessId
---- ----------- ---------
AdobeARMservice armsvc 5524
AppIDSvc svchost 1960
Appinfo svchost 13852
AudioEndpointBuilder svchost 4324
Audiosrv svchost 4456
(...)
Dienstgruppen
Wie Sie vielleicht bemerkt haben, nutzen viele Dienste denselben Prozess, und es gibt viele Prozesse mit identischen Namen („svchost“). Das ist so beabsichtigt; in Windows können viele Dienste innerhalb derselben DLL ausgeführt werden.
Um ihre Dienstgruppe abzurufen, können wir Get-ServiceProcessId wiederverwenden. Dieses Mal lesen wir nicht die Prozess-ID aus den WMI-Objekten, sondern den PathName:
function Get-ServiceGroupName
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]
[string]
$Name,
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]
[System.ServiceProcess.ServiceController]
$Service,
[switch]
$PassThru
)
begin
{
$hash = Get-CimInstance Win32_Service |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
# since the user now has the option to either specify the service name
# or pipe in an actual service, let's look up the pendant so we always
# have both and can simplify the remaining code
if ($PSCmdlet.ParameterSetName -eq 'ServiceName')
{
$Service = Get-Service -Name $Name -ErrorAction Ignore
}
else
{
$Name = $Service.Name
}
# get the launch command for this service
$pathName = $hash.$Name.PathName
# look if the parameter "-k" was specified, followed by the service group name
# we are after (uses RegEx)
$groupName = if($pathName -match '-k\s+(\w+)')
{
$matches[1]
}
if ($PassThru)
{
# take the service and add the process ID
$Service |
Add-Member -MemberType NoteProperty -Name GroupName -Value $groupName -PassThru
}
else
{
$groupName
}
}
}
Und da wir modulare, pipeline-fähige Skripterstellung verwendet haben, erhalten wir ein weiteres, Lego-ähnliches Codefragment, das wir nach Bedarf mit den anderen kombinieren können:
PS C:\> Get-Service | Get-ServiceProcessId -PassThru | Get-ProcessNameById -PassThru | Get-ServiceGroupName -PassThru | Where-Object ProcessId | Select-Object -Property Name, ProcessName, GroupName, ProcessId
Name ProcessName GroupName ProcessId
---- ----------- --------- ---------
AdobeARMservice armsvc 5524
AppIDSvc svchost LocalServiceNetworkRestricted 1960
Appinfo svchost netsvcs 13852
AudioEndpointBuilder svchost LocalSystemNetworkRestricted 4324
Audiosrv svchost LocalServiceNetworkRestricted 4456
(...)
PowerShell Universal als Rettung
Sie haben vielleicht einige Stunden Arbeit investiert und tolle PowerShell-Skripte erstellt, die Ihnen die benötigten Informationen liefern. Aber werden Sie sich nächste Woche noch an all das erinnern? Nächsten Monat? Was, wenn Sie diese Informationen nur in der Produktion benötigen? Oder wenn Sie möchten, dass ein Helpdesk-Kollege sie einsehen kann, ohne ihn in Konsolen und PowerShell-Skripte einführen zu müssen?
Hier kommt PowerShell Universal ins Spiel. Es ist für den privaten Gebrauch völlig kostenlos. Für die kommerzielle Nutzung wird es kostengünstig pro Server und nicht pro Arbeitsplatz lizenziert.
Kurz gesagt, PowerShell Universal integriert einen PowerShell-Host in einen Webserver. So können Sie sicher nutzerfreundliche Dashboards erstellen, Skripte in Dienste und Webdienste umwandeln und vieles mehr.
Fürs Erste möchten wir die kostenlose Personal Edition nutzen, um ein nutzerfreundliches GUI-Dashboard zu erstellen, in dem wir die soeben abgerufenen Serviceinformationen anzeigen können. Denken Sie bitte daran, dass dies nur ein Beispiel ist. Selbstverständlich können Sie unseren Code durch alle Informationen ersetzen, die Ihnen wichtiger sind und VMWare, Bildersammlungen oder Nutzerkonten auf dieselbe Weise darstellen.
Einrichten von PSU
Hier sind die wenigen Schritte, die zur Einrichtung von PSU auf Ihrem Rechner erforderlich sind. Keine Lizenz, keine Kosten, keine Administratorrechte erforderlich. Sie können das PowerShell Universal-Modul verwenden, um den Universal-Server zu installieren. Verwenden Sie dazu Install-Module oder Install-PSResource.
Install-Module Devolutions.PowerShellUniversal
Um den Universal-Server zu installieren, können Sie Install-PSUServer verwenden.
Install-PSUServer -LatestVersion
Die Ausführung dieses Befehls hat je nach Betriebssystem unterschiedliche Auswirkungen:
- Unter Windows wird ein Windows-Dienst auf Ihrem Rechner erstellt und gestartet.
- Unter Linux wird ein systemd-Dienst erstellt und gestartet.
- Unter macOS wird der PowerShell Universal-Server heruntergeladen und extrahiert. Weitere Installationsoptionen finden Sie in der Dokumentation unter Installation | PowerShell Universal .
Erstellen einer PSU-App
Um Ihrem Skript eine grafische Nutzeroberfläche hinzuzufügen, öffnen Sie die PSU-Verwaltungskonsole unter http://localhost:5000. Dafür müssen Sie sich mit Ihrem zuvor erstellten Devolutions-Konto anmelden.
Klicken Sie links auf „Apps“ und dann auf „App“. Rechts sehen Sie nun eine Liste der Apps, die Sie zuvor erstellt haben. Diese Liste ist anfangs leer.
Klicken Sie auf „APP erstellen“. Geben Sie ihr einen aussagekräftigen Namen, z. B. „Service-Dashboard“. Tragen Sie im Feld darunter die URL Ihrer App ein. Diese muss einen relativen Namen haben und mit einem Schrägstrich „/“ beginnen, z. B. „/list-services“. Das Feld „Beschreibung“ können Sie leer lassen. Stellen Sie sicher, dass der Schieberegler „Definition“ auf „Code“ und nicht auf „Befehl“ eingestellt ist. Klicken Sie anschließend auf „OK“. Die neue App wird nun in Ihrer App-Liste angezeigt.
Klicken Sie nun auf das Stiftsymbol. Dadurch wird der integrierte PowerShell-Editor geöffnet. Der Standardcode lautet:
New-UDApp -Content { 'Hello, World!' }
Um diese App zu testen, klicken Sie oben auf die Schaltfläche „App anzeigen“. Ihr webbasiertes PowerShell-Skript wird ausgeführt, und Sie sehen eine freundliche Meldung in Ihrem Browser. Außerdem sehen Sie, wie Sie auf diese App zugreifen können: Die URL lautet http://localhost:5000/list-services/Home.
Wichtige Hinweise
Bevor wir fortfahren, hier zwei wichtige Dinge, die Sie beachten sollten:
- Der Standardbeispielcode verwendet
New-UDApp {}, um eine einfache Meldung anzuzeigen. Dieses Cmdlet muss immer vorhanden sein, da es die App erstellt. Alle Ihre Anpassungen erfolgen ausschließlich innerhalb der geschweiften Klammern. - Sie können Ihren Code jederzeit testen, indem Sie oben auf der Editorseite auf die Schaltfläche „App anzeigen“ klicken. Aber zuvor müssen Sie auf das SPEICHERN-Symbol klicken. „App anzeigen“ speichert Ihre letzten Änderungen nicht automatisch. Wenn Sie vergessen, auf SPEICHERN zu klicken, werden Ihre Codeänderungen von „App anzeigen“ ignoriert.
Eine Tabelle anzeigen
Ersetzen Sie den Standardcode innerhalb der geschweiften Klammern durch diesen Code:
New-UDApp -Content {
# 1.) prepare the data to show
# get all services...
$data = Get-Service -ErrorAction Ignore |
Sort-Object -Property DisplayName |
# ...select the properties to display in the table
Select-Object -Property DisplayName, Name, StartMode, StartType
# 2.) output it using one of the many available UI components, i.e. as a table
New-UDTable -Data $data
}
Der Code ruft Informationen ab und speichert sie in $data. Anschließend ruft er eines der zahlreichen UD…-Cmdlets auf, um die Daten in Ihrer Webanwendung anzuzeigen. New-UDTable erstellt beispielsweise automatisch eine Tabelle.
Sobald Sie auf „App anzeigen“ klicken, können Sie die Tabelle in Ihrem Browser anzeigen:
Vielleicht ist Ihnen aufgefallen, dass wir -ErrorAction Ignore zu unserem Code hinzugefügt haben. Das liegt daran, dass PSU Ihren Code standardmäßig in PowerShell 7 ausführt. PowerShell 7 verhält sich etwas anders als Windows PowerShell, und Get-Service kann auf Systemen außerhalb der USA Ausnahmen auslösen. Immer wenn in Ihrem Code Ausnahmen auftreten, zeigt die App diese Fehlermeldungen an. Um dies zu vermeiden, stellen Sie bitte stets sicher, dass Ihr Code keine unbehandelten Ausnahmen auslöst.
Dienst-Viewer
Nachdem Sie nun gesehen haben, wie einfach es ist, Ihrem Code eine grafische, browserbasierte Nutzeroberfläche hinzuzufügen, lassen Sie uns alles zusammenfassen und unseren erweiterten Dienstprozess-Viewer erstellen. Ersetzen Sie den Code durch den zuvor erstellten Code:
# first, define the functions we created
# these functions could also be stored in external modules so they are
# out of the way, but for now we keep everything as a single script
# submit name of service or pipe in services
# adds the process ID of the process that executes a service
function Get-ServiceProcessId {
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]
[string]
$Name,
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]
[System.ServiceProcess.ServiceController]
$Service,
[switch]
$PassThru
)
begin
{
$hash = Get-CimInstance Win32_Service -ErrorAction Ignore |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
# since the user now has the option to either specify the service name
# or pipe in an actual service, let's look up the pendant so we always
# have both and can simplify the remaining code
if ($PSCmdlet.ParameterSetName -eq 'ServiceName')
{
$Service = Get-Service -Name $Name -ErrorAction Ignore
}
else
{
$Name = $Service.Name
}
# make sure "0" values are replaced by NULL
$id = $hash.$Name.ProcessId | Where-Object { $_ -gt 0 }
if ($PassThru)
{
# take the service and add the process ID
$Service |
Add-Member -MemberType NoteProperty -Name ProcessId -Value $id -PassThru
}
else
{
$id
}
}
}
# pipe in any object with a property "ProcessId" or "Id", and add a property with
# the actual process name
function Get-ProcessNameById {
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[Object]
$InputObject,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[int]
[Alias('Id')]
$ProcessId,
[switch]
$PassThru
)
process
{
$ProcessName = if ($ProcessId)
{
(Get-Process -Id $ProcessId).Name
}
if ($PassThru)
{
$InputObject |
Add-Member -MemberType NoteProperty -Name ProcessName -Value $ProcessName -PassThru
}
else
{
$ProcessName
}
}
}
# submit the name of a service, or pipe in services
# adds the property "GroupName", exposing the name of the service group for
# the given service
function Get-ServiceGroupName
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ServiceName')]
[string]
$Name,
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ActualService')]
[System.ServiceProcess.ServiceController]
$Service,
[switch]
$PassThru
)
begin
{
$hash = Get-CimInstance Win32_Service |
Group-Object -Property Name -AsHashTable -AsString
}
process
{
# since the user now has the option to either specify the service name
# or pipe in an actual service, let's look up the pendant so we always
# have both and can simplify the remaining code
if ($PSCmdlet.ParameterSetName -eq 'ServiceName')
{
$Service = Get-Service -Name $Name -ErrorAction Ignore
}
else
{
$Name = $Service.Name
}
# get the launch command for this service
$pathName = $hash.$Name.PathName
# look if the parameter "-k" was specified, followed by the service group name
# we are after (uses RegEx)
$groupName = if($pathName -match '-k\s+(\w+)')
{
$matches[1]
}
if ($PassThru)
{
# take the service and add the process ID
$Service |
Add-Member -MemberType NoteProperty -Name GroupName -Value $groupName -PassThru
}
else
{
$groupName
}
}
}
# NOW we are defining our web app
# thanks to our modular functions, this part is short and simple to
# understand and modify
New-UDApp -Content {
# 1.) prepare the data to show
# retrieve all services (make sure to catch any error)
$data = Get-Service -ErrorAction Ignore |
# ...add the service process ID...
Get-ServiceProcessId -PassThru |
# ...take only services that have a process ID...
Where-Object ProcessId |
# ...add the process name for the process ID...
Get-ProcessNameById -PassThru |
# ...add the service group name so we can differentiate svchost...
Get-ServiceGroupName -PassThru |
# ...sort by service displayname...
Sort-Object -Property DisplayName |
# select the properties to display in the table
Select-Object -Property DisplayName, Name, ProcessName, GroupName, ProcessId
# 2.) output it using one of the many available UI components, i.e. as a table
New-UDTable -Data $data
}
Am Anfang finden Sie drei Funktionen, die wir zuvor erstellt haben. In einem späteren Teil dieser Serie werden wir diese Funktionen auslagern und in externe Module verschieben, aber im Moment ist alles im Code definiert, sodass wir keine Abhängigkeiten haben.
Die abgerufenen Daten werden anschließend mit derselben New-UDTable-Tabelle angezeigt, die Sie zuvor verwendet haben. Wenn Sie auf „App anzeigen“ klicken, wird Ihr Code ausgeführt, die Dienstinformationen werden abgerufen und die Daten werden Ihnen in einer Tabelle präsentiert.
Sie benötigen lediglich die URL dieser Webanwendung: http://localhost:5000/list-services/Home. Sie können sie als Verknüpfung auf Ihrem Desktop speichern oder in Ihrem Browser als Lesezeichen hinzufügen. Wenn Sie Dienstinformationen benötigen, müssen Sie nicht mehr nach Skripten suchen und diese in der Konsole ausführen.
Und wenn Sie dies einmal verinnerlicht haben, werden Sie die enormen Möglichkeiten erkennen: Sie können Ihre persönlichen Dashboards erstellen. Mit einer Unternehmenslizenz und der Einrichtung von PSU mit Administratorrechten können Sie Ihre Skripte aber auch als Dienst bereitstellen und so jedem in Ihrem Unternehmen browserbasierten Zugriff mit detaillierter Sicherheitskontrolle ermöglichen.
Wir werden dieses Thema im Laufe der Serie behandeln. Sie haben nun gesehen, wie Sie PSU einrichten und Ihre erste eigene PSU-Webanwendung erstellen. Im nächsten Teil beginnen wir erneut mit PowerShell-Code und erweitern das Dashboard anschließend um neue Steuerelemente und zusätzliche Funktionen. Wir erklären Ihnen außerdem, warum die soeben erstellte Diensttabelle die Spalten in einer etwas unerwarteten Reihenfolge anzeigt und wie Sie dies korrigieren können.

Tobias Weltner
Aleksandar Nikolić
Adam Driscoll
