Private/Wissen/B_Basic/B17_Objekt-Handling.ps1

<#
 
# Objekt-Handling
 
Der richtige Umgang mit Objekten in der PowerShell.
 
- **Hashtags** Object Where Select Sort Group Measure Compare ForEach New Analyse static inherits
 
- **Version** 2020.02.29
 
#>


# READ Weiterführende und Nachschlage-Informationen:

Get-Help -Name 'about_Objects' -ShowWindow
Get-Help -Name 'about_Object_Creation' -ShowWindow
Get-Help -Name 'about_Pipelines' -ShowWindow
Get-Command -Noun 'Object' -Module 'Microsoft.PowerShell.*'

#region Objekte-Analyse

# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# ! Es ist wichtig im Vorfeld zu analysieren: !
# ! A) Um welche Art (Type) von (Rückgabe-)Objekt es sich handelt (Get-Member (gm)). !
# ! B) Was mit diesem Objekt möglich ist (Properties, Methods, Events) (Get-Member (gm)). !
# ! C) Welche Werte enthalten die Eigenschaften (Select-Object, (select))? !
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

#region BEISPIEL 1: Wann ist der Zielhost erreichbar?

Test-NetConnection -ComputerName 127.0.0.1 | Get-Member                # zu A) siehe: TypeName: TestNetConnectionResult
Test-NetConnection -ComputerName 127.0.0.1 | Get-Member                # zu B) siehe: Name, MemberType, Definition
Test-NetConnection -ComputerName 127.0.0.1 | Select-Object -Property * # zu C) siehe: PropertyName : PropertyValue

# ! ANTWORT: Wenn die Property 'PingSucceeded' den Wert 'true' enthält, dann ist der Zeilhost erreichbar:
$result = Test-NetConnection -ComputerName 127.0.0.1 | Select-Object -ExpandProperty 'PingSucceeded'
if($result -eq $true) { 'Mach was auf dem Zielhost!' }

#endregion

#region BTW: .EXE-Befehle meiden, da diese NUR String-Objekte zurück liefern und daher nur String-Operationen möglich sind:

ping.exe 127.0.0.1 | Get-Member # => String => String-Operationen

# ! '.exe'-Befehle liefern nur String-Auflistungen zurück deren Weiterverarbeitung umständlich ist:
$result = ping.exe 127.0.0.1 | Select-String -Pattern 'Verloren = 0' | Measure-Object | Select-Object -ExpandProperty Count
if($result -gt 0) { 'Mach was auf dem Zielhost!' }

#endregion

#region BEISPIEL 2: Welche Prozesse sind von der Firma Microsoft?

# TODO ANALYSIEREN Welche Objekte 'Get-Process' zurück liefert und welche Properties brauchbare Werte enthalten könnte:
Get-Process | Get-Member # ! ZUM BEISPIEL: System.Diagnostics.Process (ProcessName, Company, Description, Product, Site)

# TODO ANALYSIEREN Welche tatsächlichen Werte enthalten die vermeintlichen 'brauchbaren' Properties:
Get-Process | Select-Object -Property 'ProcessName', 'Company', 'Description', 'Product', 'Site'

# TODO Betroffene Property 'Company' mit dem Filterwert 'Microsoft Corporation' anwenden:
Get-Process | Where-Object -Property 'Company' -IEQ -Value 'Microsoft Corporation'

#endregion

# TIPP Da Get-Member und Select-Object temporär oft benutzt wird steigern Sie Ihre Effizienz, wenn Sie deren Aliase (gm, select) benutzten!

# ? GET-MEMBER: Von welchem Typ ist ein Objekt?

# ! TypeName : Vollständiger Type-Name
# ! z.B.: System .IO .FileInfo
# ! NAMESPACE.NAMESPACE.TYPENAME d.h. Für weitere Beschreibung nach 'System.Io.FileInfo' googeln

# ? GET-MEMBER: Was kann man mit dem Objekt machen (Methode)
# ? GET-MEMBER: Welche Information trägt das Objekt (Property)
# ? GET-MEMBER: Über welche Ereignisse kann ich informiert werden (Event)

# ! Name => Name des Member's
# ! MemberType => Art des Members: Property <TypeName> PropertyName {get; set;}
# ! Methode <void> => Ohne Rückgabe
# ! <Rückgabetyp> MethodName(EingabeArgumentTypen)
# ! Definition => Beschreibung

#region BEISPIEL 3: Wie können alle .LOG-Dateien in C:\Windows angezeigt werden?

#! 1.) Analysieren (Eingrenzen & Bestimmen):
Get-ChildItem -Path 'C:\Windows' -File | Get-Member
# * => System.IO.FileInfo (Name, Extension, FullName)

#! 2.) Analysieren (Wertebereich festlegen):
Get-ChildItem -Path 'C:\Windows' -File | Select-Object -Property 'Name', 'Extension', 'FullName'

#! 3.) 1. und 2. Anwenden:
Get-ChildItem -Path 'C:\Windows' -File | Where-Object -Property 'Extension' -IEQ -Value '.LOG'

#endregion

#region BEISPIEL 4: Eine Übersicht aller großen Dateien anzeigen:

# ! Analyse + Rückschlüsse:

Get-ChildItem -Path 'C:\' -Force -Recurse -File | Get-Member
Get-ChildItem -Path 'C:\' -Recurse -File -Force | Select-Object -First 1 | Get-Member
Get-ChildItem -Path 'C:\' -Recurse -File -Force | Select-Object -First 10 -Property 'FullName', 'Length', 'Attributes'

# ! Anwendung:

Get-ChildItem -Path 'C:\' -Recurse -File -Force -ErrorAction 'Ignore' | Where-Object -Property 'Length' -GE -Value 50MB

#endregion

#region BEISPIEL 5: Eine Übersicht aller Dateien die älter sind als 1 Jahr sind:

# ! Analyse + Rückschlüsse:

Get-ChildItem -Path 'c:\' -Recurse -File -Force -ErrorAction 'Ignore' | Select-Object -First 1 | Get-Member
Get-ChildItem -Path 'c:\' -Recurse -File -Force -ErrorAction 'Ignore' | Select-Object -First 1 -Property 'Name', 'CreationTime', 'LastWriteTime', 'LastAccessTime'
Get-Date | Get-Member
(Get-Date).AddYears(-1)

# ! Anwendung:

Get-ChildItem -Path 'c:\' -Recurse -File -Force -ErrorAction 'Ignore' | Where-Object -Property 'LastWriteTime' -LE -Value (Get-Date).AddYears(-1)

#endregion

#endregion

#region Objekt-Analyse für Profis

#region TYPE: Enumeration

# ! PowerShell 7 - Es gibt eine Autovervollständigung (CTRL + SPACE) für das Zuweisen von Aufzählungs-Werten (Enum) zu Variablen:

$ErrorActionPreference = 'Stop'
# ? ^ An dieser Stelle in der Console CTRL+SPACE drücken.

# ! Enumerationen (Enum) bilden eine häufig benutzt Untermenge von Typen da. Ob ein Parameter eben nur diese Enumeration akzeptiert ist auf den ersten Blick nicht ersichtlich, da die Hilfe nur den Typennamen ausgibt.

# ? Handelt es sich bei einem Type X um eine Enumeration?

$ErrorActionPreference | Get-Member
[System.Management.Automation.ActionPreference].IsEnum
[System.Management.Automation.ActionPreference].GetEnumNames()
# ! Wenn die Antwort JA lautet erfolgte eine Zuweisung wie folgt:
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

# ? Handelt es sich bei einem Type X um eine Enumeration?

Get-Service | Get-Member -Name StartType
[System.ServiceProcess.ServiceStartMode].IsEnum
[System.ServiceProcess.ServiceStartMode].GetEnumNames()
# ! Wenn die Antwort JA lautet erfolgte ein Vergleich wie folgt:

#region BEISPIEL 6: Alle Dienste deren Starttyp auf Manuell steht anzeigen:

# ! Analyse + Auswertung:

Get-Service | Get-Member
Get-Service | Select-Object -Property 'Name', 'StartType'

# ! Anwenden:

# Problematisch da eine automatische Konvertierung nach ServiceStartMode statt findet...:
Get-Service | Where-Object -Property 'StartType' -IEQ -Value 'Manual'
# TIPP - ... besser so:
Get-Service | Where-Object -Property 'StartType' -EQ -Value ([System.ServiceProcess.ServiceStartMode]::Manual) 

#endregion

#region Vorhandene Enumerationen finden

function Get-Enum {
    param ([string]$Value, [String]$Name, [switch]$All)
    $defaultManifestModules = 'CommonLanguageRuntimeLibrary', 
                              'Microsoft.CSharp.dll', 
                              'Microsoft.Management.Infrastructure.dll', 
                              'Microsoft.PowerShell.Commands.Management.dll', 
                              'Microsoft.PowerShell.commands.Utility.dll', 
                              'System.dll', 
                              'System.Configuration.dll',
                              'System.Configuration.Install.dll',
                              'System.Core.dll', 
                              'System.Data.dll', 
                              'System.DirectoryServices.dll', 
                              'System.Management.Automation.dll', 
                              'System.Management.dll',
                              'System.ServiceProcess.dll',
                              'System.Transactions.dll', 
                              'System.Xml.dll'
    
    [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object -FilterScript {$All -or ($defaultManifestModules -contains $_.ManifestModule)} | ForEach-Object -Process { try { $_.GetExportedTypes() } catch { 'Keine ExportedTypes vorhanden' | Write-Verbose } } | Where-Object -FilterScript { $_.IsEnum -and $_.Name -imatch $Name } | ForEach-Object -Process {
            return [PSCustomObject]@{
                Name   = $_.FullName
                Source = $_.Module.ScopeName
                Values = [System.Enum]::GetNames($_)
            }
        } | Where-Object -Property Values -imatch -Value $Value
 }

Get-Enum -Value 'SilentlyContinue'
Get-Enum -Name 'ServiceStartMode'
Get-Enum -All | Measure-Object
 
#endregion

# ! Es gibt eine Autovervollständigung (CTRL + SPACE) für das Zuweisen von Aufzählungs-Werten (Enum) zu Variablen seit PowerShell 7:

$ErrorActionPreference = 'Stop'
# ? ^ An dieser Stelle in der Console CTRL+SPACE drücken.

#endregion

#region TYPE: Class

# !!! INHERITANCE !!!

# ! 1. Eine Klasse kann von EINER anderen Basisklasse ABGELEITET sein und ERBT so dessen Member/Funktionen.

# ! 2. Jede Klasse ist früher oder später von der Klasse 'Object' (alias PSObject) abgeleitet worden.

# ! 3. Eine abgeleitete Klasse A ist daher mit der Basisklasse B kompatibel, daher kann auch ein Objekt von Typ A an einen Parameter vom Typ B übergeben werden.

# ? Welche Typ A von welchem Typ B abgeleitet wurde erfahren Sie so:

"Köln!".PSTypeNames

(Get-Service)[0].PSTypeNames

(Get-Process)[0].PSTypeNames

(Get-Process).PSTypeNames

# !!! STATIC MEMBER !!!

# ! Objekte werden von einer bestimmten Klasse instansziert und so materialisiert. Erst dann können wir mit diesen Objekten in der PowerShell hantieren. Es gibt jedoch statische Member die direkt an der Klasse aufrufen werden können ohne das ich zuerst eine OBjekt erzeugen muss.

# ? Welche statische Member besitzt eine Klasse:
Get-Process | Get-Member -Static
[System.Diagnostics.Process]::Start('notepad')

Get-Date | Get-Member -Static
[datetime]::Today

# !!! OVERLOADING !!!

# ! Eine Methode kann mehrfacht überladen sein. Unter dem gleichen Methoden-Namen wird unterschiedliches Methoden-Verhalten implementiert. Wichtig ist jedoch das die Parameter-Signatur eindeutig ist.

# ? Überladungs-Definitionen einer Methode anzeigen
$Text = "PowerShell"
$Text | Get-Member -Name LastIndexOf
$Text.LastIndexOf.OverloadDefinitions

#endregion

#region GET-MEMBER: Hintergrundwissen

# ? Unterschied zwischen der Analyse von Pipeline-Objekten und NICHT-Pipeline-Objekten:
1..99 | Get-Member              # ! Analysiert die Objekte in der Pipeline
# vs.
Get-Member -InputObject (1..99) # ! Analysiert DAS Objekte (1..99)

#endregion

#region Hilfereiche Erweiterungen zur Analyse

# ! Diese Methode wird automatisch beim laden des AKPT-Moduls implementiert
$code = {
    Add-Type -AssemblyName "System.Windows.Forms"

    $propertyGrid                            = New-Object -TypeName "System.Windows.Forms.PropertyGrid"
    $propertyGrid.Dock                       = [System.Windows.Forms.DockStyle]::Fill
    $propertyGrid.PropertySort               = [System.Windows.Forms.PropertySort]::Alphabetical
    $propertyGrid.CanShowVisualStyleGlyphs   = $true
    $propertyGrid.CommandsVisibleIfAvailable = $true
    $propertyGrid.HelpVisible                = $true
    $propertyGrid.ToolbarVisible             = $true
    $propertyGrid.SelectedObject             = $this

    $window         = New-Object -TypeName "System.Windows.Forms.Form"
    $window.Text    = $this.ToString()
    $window.Size    = New-Object -TypeName "System.Drawing.Size" -ArgumentList @(600, 800)
    $window.TopMost = $true
    $window.Controls.Add($propertyGrid)
    $window.ShowDialog() | Out-Null
}
Update-TypeData -MemberType "ScriptMethod" -MemberName "ShowObject" -Value $code -TypeName "System.Object" -Force 
  
# ! Beispiel-Aufruf:
$p1 = Get-Process | Select-Object -First 1
$p1.ShowObject()

# ! Diese Methode wird automatisch beim laden des AKPT-Moduls implementiert
$code = {
    $url = 'https://docs.microsoft.com/de-de/dotnet/api/{0}' -f $this.GetType().FullName
    Start-Process $url
}
Update-TypeData -MemberType "ScriptMethod" -MemberName "GetHelp" -Value $code -TypeName "System.Object" -Force
  
# ! Beispiel-Aufruf:
$p1 = Get-Process | Select-Object -First 1
$p1.GetHelp()

#endregion

#endregion

#region Mit den Default-Cmdlets (*-Object) Rückgabe-Objekte zu managen

# ? Eine Übersicht von Cmdlets die Rückgabe-Objekte managen können:
Get-Command -Name Get-Command -Noun Object -Module Microsoft.PowerShell.*
Get-Command -Name Where-Object   -Syntax # Filtern (Zeile)
Get-Command -Name Select-Object  -Syntax # Filtern (Spalten, First, Last, ExpandProperty, ...)
Get-Command -Name Sort-Object    -Syntax # Sortieren
Get-Command -Name Group-Object   -Syntax # Gruppieren
Get-Command -Name Measure-Object -Syntax # Messen (Count, Sum, Min, Max, Avg)
Get-Command -Name Compare-Object -Syntax # Vergleichen
Get-Command -Name ForEach-Object -Syntax # Schleife
Get-Command -Name New-Object     -Syntax # Erstellen
Get-Command -Name Tee-Object     -Syntax # Verzweigen & loggen

#region Sort-Object

Get-Process | Sort-Object -Property Name
Get-Process | Sort-Object -Property WorkingSet64 -Descending
Get-Process | Sort-Object -Property WorkingSet64 -Descending | Select-Object -First 3

#endregion

#region Select-Object

Get-Process | Select-Object -Property Name, WorkingSet64 # d.h. ALLE anderen Eigenschaften werden entfernt
Get-Process | Select-Object -Property Name -Unique
Get-Process | Select-Object -ExpandProperty Name ; <# vs.#> ; Get-Process | Select-Object -Property Name

Get-Process | Select-Object -Skip 3 -First 2 # 4. und 5. Prozess
Get-Process | Select-Object -SkipLast 5 # Alle außer die letzten 5
Get-Process | Select-Object -Last 2
Get-Process | Select-Object -Index 5 # 0-Basierend! (Get-Process)[5]

#endregion

#region Where-Object

# Einfache Schreibweise
Get-Process | Where-Object -Property Company -Like -Value "Microsoft*"
Get-Process | Where-Object           Company -Like        "Microsoft*"
Get-Process | Where-Object -Like Company "Microsoft*"
Get-Process | Where-Object "Microsoft*" -Like Company
Get-Process | Where-Object -Like "Microsoft*" Company

# -or | -and: Ausführliche Schreibweise seit PS1.0
Get-Process | Where-Object -FilterScript { $_.Company -like "Microsoft*" -or $_.Company -like "Oracle*" }
# ! Die Variable {$_} ist die Laufzeit-Variable in der Pipeline und stellt das übergebene Objekt dar!

#endregion

#region Group-Object (Gruppiert zu Objektgruppen)

Get-Service | Group-Object -Property 'Status' 

Get-ChildItem -Path 'C:\Windows' -File -Force | Group-Object -Property 'Extension' | Sort-Object -Property 'Count' -Descending | Select-Object -First 3

#endregion

#region Compare-Object

Start-Process -FilePath 'calc'
$x = Get-Process
Stop-Process -Name '*Calc*'
Start-Process -FilePath "notepad.exe"
$y = Get-Process

Compare-Object -ReferenceObject $x -DifferenceObject $y -Property 'Name'
Compare-Object -ReferenceObject $x -DifferenceObject $y -ExcludeDifferent -IncludeEqual

#endregion

#region New-Object

Add-Type -AssemblyName System.Windows.Forms
$o1 = New-Object -TypeName 'System.Windows.Forms.Form' # Erstellt ein Objekt aus der .NET-Framework-Klasse 'Form'
$o1.ShowDialog() # .NET Wissen

$o2 = New-Object -ComObject 'Excel.Application'
$o2.Visible = $true # VBA for Application

#endregion

#region ForEach-Object

31, 32, 33, 34, 35, 36 | ForEach-Object -Process { "192.168.178.$_" }
31..36 | % { "192.168.178.$_" }

#? Eine Übersicht aus Name, Größe und Besitzer von c:\temp

Get-ChildItem -Path "C:\Temp" -File | Get-Member
Get-ChildItem -Path "C:\Temp" -File | Get-Acl | Get-Member

Get-ChildItem -Path "C:\Temp" -File | ForEach-Object -Process {
    $_ | 
        Select-Object -Property 'Name', 'Length', @{Label = 'Owner' ; Expression = { $_ | Get-Acl | Select-Object -ExpandProperty 'Owner' }} 
} | Get-Member

#endregion

#region Tee-Object

Get-Process | Tee-Object -FilePath c:\temp\EvtlBeendeteProzesse.txt | Stop-Process -WhatIf
# ! NACHTEIL 1: Eine Umleitung ist nur in Dateien möglich
# ! NACHTEIL 2: Z.Bsp. Die Auswirkungen von Stop-Process sind im 'Log' nicht enthalten!

# ? Eine andere Lösung könnte sein:
Get-Process | Stop-Process -WhatIf -PassThru | Set-Content -Path c:\temp\BeendeteProzesseInklFehler.txt
# ... Oder per ForEach-Object

#endregion

#endregion

#region Vorhandene Typen/Objekte erweitern

# ! Die objektorientierte Programmierung (OOP) ist ein, nicht wegzudenkender Bestandteil der PowerShell. Wenn wir mit Objekten in der Shell arbeiten, nutzen wir deren Member um Werte auszulesen oder Aktionen auszulösen. Was ist, wenn die Eigenschaft oder Methode nicht vorhanden ist?

# ? Welche Member besitzt ein Process-Objekt
Get-Process | Get-Member -View 'Adapted'

# ! Wenn nicht, kein Problem! Das Typen-System der PowerShell ist flexibel. Wir erweitern den Typ selbst oder das aktuelle Objekt. Dieses Verfahren hat den Vorteil, dass wir nicht komplett neue Typen erzeugen müssen. Um in diesen anschließend alles bündeln was wir für die nächsten Schritte benötigen.

# ? Welche erweiterten Member besitzt ein Process-Objekt
Get-Process | Get-Member -View 'Extended'

# ? Gruppiert nach Member-Type inkl. Anzahl
Get-Process | Get-Member -View "All" | Group-Object -Property "MemberType" -NoElement | Sort-Object -Property "Count" -Descending

<#
Die Typen-Erweiterung kann auf unterschiedliche Arten erfolgen. In einem Anwendungsfall ist es nötig das Ergebnis unmittelbar bei Objekt-Erstellung zu berechnen und in eine Eigenschaft speichern. In anderen Fällen soll die auftretende Latenz der Berechnung beim unmittelbaren Zugriff auf die Eigenschaft statt finden.
 
    * AliasProperty => Alias für eine andere Property
    * NoteProperty => Einfache R/W Property
    * ScriptMethode => ScriptBlock-Code der beim Aufruf ausgeführt wird inkl. Parameter/Argumenten-Übergabe
    * ScriptProperty => ScriptBlock-Code der beim Aufruf ausgeführt wird
    * PropertySet => Eine Gruppe von Properties
#>


# ? Für eine Typenerweiterung benötigen wir ein Objekt, z.B. der erste Process:
$p1 = Get-Process | Select-Object -First 1

# ? Dem Objekt eine NoteProperty hinzufügen:
$p1 | Add-Member -Name 'Jetzt_A' -Value ( Get-Date ) -MemberType  'NoteProperty'

# ? Dem Objekt eine ScriptProperty hinzufügen:
$p1 | Add-Member -Name 'Jetzt_B' -Value { Get-Date } -MemberType 'ScriptProperty'

# ? Dem Objekt eine ScriptMethod hinzufügen:
$p1 | Add-Member -Name 'Jetzt_C' -Value { param([int]$Year) return Get-Date -Year $Year } -MemberType 'ScriptMethod'

# ? Neue Member anzeigen:
$p1 | Get-Member -Name 'Jetzt*' -View 'Extended'

# * Beispiel testen:
$p1.Jetzt_A       # ? NoteProperty aufrufen => statisches Datum
$p1.Jetzt_B       # ? ScriptProperty aufrufen => dynamisches Datum
$p1.Jetzt_C(2050) # ? ScriptMethod aufrufen => dynamisches Datum

<#
Bis hierhin erweiterte ich Objekte direkt in der Pipeline oder in Variablen (Add-Member). Diese Art von Erweiterung wirkt sich nicht auf andere Objekte des gleichen Typs aus. Auf der einen Seite kann dieses Verhalten ein Vorteil darstellen. Obwohl vom gleichen Typ grenzen sich diese Objekte gegenseitig ab.
Wenn n-Objekte vom gleichen Typ gleiche Erweiterung erhalten, ist es nach diesem Verfahren (Add-Member) aufwendig. In diesen Fällen nehmen wir die Erweiterung direkt am Typ vor (Update-TypeData). Die Erweiterung wirkt sich in der Session auf alle Objekte vom Type X aus.
#>


# ? Session-weite Erweiterungen
Update-TypeData -TypeName "System.Diagnostics.Process" -MemberName "WorkingSet64MB" -Value { [int]($this.WorkingSet64 / 1MB) } -MemberType "ScriptProperty" -Force

# ? Alle Process-Objekt haben eine neue Property WorkingSet64MB
Get-Process | Get-Member -View "Extended"
Get-Process | Select-Object -Property 'Name', 'WorkingSet64MB'

#endregion

#region Auf Ereignisse reagieren

#region Asynchrones Ereignis-Handling

$np = Start-Process -FilePath notepad -WindowStyle Minimized -PassThru
$np | Get-Member -MemberType Event
$job = Register-ObjectEvent -InputObject $np -EventName Exited -Action { 'Der Notepad-Prozess wurde beendet.' | Write-Warning }
$np | Stop-Process -Force
$job | Get-Job
$job | Get-EventSubscriber | Unregister-Event # oder:
$job | Remove-Job -Force

#endregion

#region Synchrones Ereignis-Handling

$np = Start-Process -FilePath notepad -WindowStyle Minimized -PassThru
Register-ObjectEvent -InputObject $np -EventName Exited -SourceIdentifier np.Exited
Wait-Event -SourceIdentifier np.Exited
"... Die Verarbeitung nach dem Beenden von Notepad wurde aufgenommen." | Write-Warning
Unregister-Event -SourceIdentifier  np.Exited

#endregion

#region Hintegrundjobs überwachen

$job = Start-Job -Name gci -ScriptBlock { Get-ChildItem -Path c:\*.log -Recurse -Force -ErrorAction Ignore | Select-Object -Property Name, Length, DirectoryName }
$job | Get-Member -MemberType Event
Register-ObjectEvent -SourceIdentifier job.StateChanged -InputObject $job -EventName StateChanged -Action {
    "GCI-Job ist fertig, STATUS = $($job.State)" | Write-Warning # TIPP - oder Send-MailMessage
    Unregister-Event -SourceIdentifier job.StateChanged
    $job | Remove-Job
}
$job | Receive-Job -Keep

#endregion

#region Ordner überwachen

# ? Dateien- / Ordner auf Veränderung überwachen
$fsw = New-Object -TypeName System.IO.FileSystemWatcher
$fsw.Path = "c:\temp"
$fsw.Filter = "*.txt"
$fsw | Get-Member -MemberType Event
$action = {
    "Dateiänderung: {0} {1}" -f $eventArgs.FullPath, $eventArgs.ChangeType | Write-Warning
}
Register-ObjectEvent -InputObject $fsw -EventName Changed -Action $action -SourceIdentifier fsw_Changed
Register-ObjectEvent -InputObject $fsw -EventName Created -Action $action -SourceIdentifier fsw_Created
Register-ObjectEvent -InputObject $fsw -EventName Deleted -Action $action -SourceIdentifier fsw_Deleted
Register-ObjectEvent -InputObject $fsw -EventName Error   -Action $action -SourceIdentifier fsw_Error
Register-ObjectEvent -InputObject $fsw -EventName Renamed -Action $action -SourceIdentifier fsw_Renamed
# ! Aufräumen
Get-EventSubscriber
Unregister-Event -SourceIdentifier fsw_*  -Force
$fsw.Dispose()
$fsw = $null
Remove-Variable -Name fsw -Force

#endregion

#region WMI Events

$action = {
    $e = $Event.SourceEventArgs.NewEvent
    "Neuer Process gestartet, ID: {0} | NAME: {1}" -f $e.ProcessId, $e.ProcessName | Write-Warning
}
Register-CimIndicationEvent -ClassName "Win32_ProcessStartTrace" -SourceIdentifier "ProcessStarted" -Action $action # ! ADMIN-Rechte
Get-EventSubscriber -SourceIdentifier "ProcessStarted"
Start-Process -FilePath "calc"
Unregister-Event -SourceIdentifier "ProcessStarted" 

#endregion

#region Eigene Events auslösen

Register-EngineEvent -SourceIdentifier myEvent -Action { "Möööp!" | Write-Warning }
New-Event -SourceIdentifier myEvent
Unregister-Event -SourceIdentifier myEvent

#endregion

#endregion

#region PowerShell 7 - Update-List

# ! Das neue Cmdlet Update-List aktualisiert Listen-Einträge (Add / Remove) von Listen-Objekt-Eigenschaften:

class OsDetails {
    [System.Collections.ArrayList]$ProcessItems
    OsDetails() {
        $this.ProcessItems = New-Object -TypeName "System.Collections.ArrayList"
    }
}

$osd = New-Object -TypeName "OsDetails"
$osd.ProcessItems.AddRange((Get-Process | Select-Object -First 3))
$osd.ProcessItems

$notepad = Start-Process "notepad" -PassThru

$osd | Update-List -Property "ProcessItems" -Add $notepad | Out-Null
$osd.ProcessItems

$osd | Update-List -Property "ProcessItems" -Remove $notepad | Out-Null
$osd.ProcessItems

#endregion

# TODO QUIZ - https://forms.office.com/Pages/ResponsePage.aspx?id=DQSIkWdsW0yxEjajBLZtrQAAAAAAAAAAAAa__Yp1xwFUME9MN1E1TTBTWEdNOE1QNloyS0paVE05Mi4u

<# TODO Übungen
 
# Übung A
 
Finden Sie alle inkl. versteckte und System-Dateien in C:\ und darunter die größer 100MB sind.
 
# TIPP - Get-ChildItem ; Where-Object
 
# Übung B
 
Beenden Sie alle Notepad- und Taschenrechner-Prozesse.
 
# TIPP - Get-Process ; Where-Object -IN ; Stop-Process -WhatIf
 
# Übung C
 
1. Öffnen Sie sämtliche .log-Dateien in C:\Windows\Logs\*.
 
2. Anschließend filtern Sie den Inhalt nach dem Wort "Error".
 
3. Und jetzt schreiben Sie diese Error-Zeile in eine neue zentral MasterError.log-Datei weg. Folgende Informationen sollen enthalten sein:
Quell-Dateiname, Zeilennummer, Fehlertext-Zeile.
 
# TIPP - Where-Object ; Select-String ; Set-Content
 
# Übung D
 
Wie alt (in Tagen) ist die älteste Datei (CreationTime) im C:\Windows-Order?
 
# TIPP - Get-ChildItem ; Sort-Object ; New-TimeSpan ; Select-Object
 
# Übung E
 
Finden Sie die ersten 10 Dateien die größer sind als 80 Megabyte und geben zu jeder Datei folgende Informationen aus:
 
DateiName, DateiBesitzer, DateiGrößeInMB, DateiAlterInTagen
 
# TIPP - Get-ChildItem ; Where-Object ; Select-Object ; Get-Acl ; Get-Date ; Add-Member ; ForEach-Object
 
# Übung F
 
Warum wird die Eigenschaft PSTypeNames von Get-Member nicht angezeigt?
 
"Köln!".PSTypeNames
 
#>