Greyhound.psm1

<##############################################################
                    helper functions
##############################################################>



function Start-ServiceEx {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]$Name,
        [Int]$MaxTimeoutSeconds=30
    )
    try {
        if (!($Service = Get-Service -Name $Name -ErrorAction SilentlyContinue)) {
            Throw "Der Dienst `"$Name`" existiert nicht."
        }

        if ($Service.Status -eq "Running") {
            Write-Verbose "Der Dienst `"$($Service.Name)`" ist bereits gestartet!"
            return
        }

        $DesiredStatus = [System.ServiceProcess.ServiceControllerStatus]::Running
        $MaxTimeout = New-TimeSpan -Seconds $MaxTimeoutSeconds

        Write-Verbose "Der Dienst `"$($Service.Name)`" wird gestartet..."
        try {
            $StartServiceJob = Start-Job -ArgumentList $Service.Name -ScriptBlock {param($ServiceName) Start-Service -Name $ServiceName}
            $Service.WaitForStatus($DesiredStatus, $MaxTimeout)
            $Service.Refresh()
        }
        catch [System.ServiceProcess.TimeoutException] {
            Throw "Der Dienst `"$($Service.Name)`" konnte innerhalb der maximalen Wartezeit von $MaxTimeoutSeconds Sekunden nicht gestartet werden."
        }
    }
    catch {
        $StartServiceJob | Remove-Job -ErrorAction SilentlyContinue
        Write-Error $($_.Exception.Message)
    }
}



function Stop-ServiceEx {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]$Name,
        [Int]$MaxTimeoutSeconds=15*60
    )
    try {
        if (!($Service = Get-Service -Name $Name -ErrorAction SilentlyContinue)) {
            Throw "Der Dienst `"$Name`" existiert nicht."
        }

        if ($Service.Status -eq "Stopped") {
            Write-Verbose "Der Dienst `"$($Service.Name)`" ist bereits gestoppt!"
            return
        }
        
        $DesiredStatus = [System.ServiceProcess.ServiceControllerStatus]::Stopped
        $MaxTimeout = New-TimeSpan -Seconds $MaxTimeoutSeconds

        Write-Verbose "Der Dienst `"$($Service.Name)`" wird gestoppt..."
        try {
            $StopServiceJob = Start-Job -ArgumentList $Service.Name -ScriptBlock {param($ServiceName) Stop-Service -Name $ServiceName}
            $Service.WaitForStatus($DesiredStatus, $MaxTimeout)
            $Service.Refresh()
        }
        catch [System.ServiceProcess.TimeoutException] {
            Write-Verbose "Der Dienst `"$($Service.Name)`" konnte innerhalb der maximalen Wartezeit von $MaxTimeoutSeconds Sekunden nicht gestoppt werden. Der Prozess wird gekillt."
            try {
                $Service = Get-WmiObject -Class win32_service -ErrorAction SilentlyContinue | Where-Object Name -eq $Name
                Stop-Process -Id $Service.ProcessId -Force -ErrorAction Stop
            }
            catch {
                Throw "Der Prozess `"$Service.ProcessId`" konnte nicht gekillt werden."
            }
        }
    }
    catch {
        $StopServiceJob | Remove-Job -ErrorAction SilentlyContinue
        Write-Error "$($_.Exception.Message)"
    }
}



function Get-MySqlVersion {
    [CmdletBinding()]
    param (
        [String]$MySqlServiceName='MySql'
    )
    try {
        $MySqlService = Get-WmiObject win32_service | Where-Object Name -eq $MySqlServiceName
        if ($MySqlService) {
            [string]$MySqldExePath = $MySqlService.PathName -replace '^"?(.*mysqld).*', '$1.exe'   
            if ($MySqldExePath) {
                $MySqldExe = Get-Item -Path $MySqldExePath -ErrorAction Stop
            } else {
                Throw "Der MySqld-Pfad konnte nicht ermittelt werden"
            }           
        }
        $MySqldExe.VersionInfo
    }
    catch {
        Write-Error "Es ist ein Fehler bei der Ermittlung der MySQL-Version aufgetreten: $($_.Exception.Message)" 
    }
}



function Get-MySqlBasedir {
    [CmdletBinding()]
    param (
        [String]$MySqlServiceName='MySql'
    )
    try {
        $MySqlService = Get-WmiObject win32_service | Where-Object Name -eq $MySqlServiceName
        if ($MySqlService) {
            [string]$MySqlBaseDir = $MySqlService.PathName -replace '^"?(.*?)\\bin.*', '$1'
        } else {
            Throw "Der Dienst `"$MySqlServiceName`" ist kein installierter Dienst."
        }

        if (!($MySqlBaseDir) -or !(Test-Path -Path "$MySqlBaseDir" -ErrorAction SilentlyContinue)) {
            Write-Warning "Das MySql-Basis-Verzeichnis konnte nicht ermittelt werden."
        } else {
            $MySqlBaseDir.TrimEnd("\")
        }
    }
    catch {
        Write-Error "Es ist ein Fehler bei der Ermittlung des MySQL-Basis-Verzeichnisses aufgetreten: $($_.Exception.Message)"  
    }
}



function Get-MySqlMyIni {
    [CmdletBinding()]
    param (
        [String]$MySqlServiceName='MySql'
    )
    try {
        $MySqlService = Get-WmiObject win32_service | Where-Object Name -eq $MySqlServiceName
        if ($MySqlService) {
            [string]$MySqlMyIni = $MySqlService.PathName -replace '.*--defaults-file="?(.*\.ini).*', "`$1"
        } else {
            Throw "Der Dienst `"$MySqlServiceName`" ist kein installierter Dienst:"
        }
        $MySqlMyIni
    }
    catch {
        Write-Error "Es ist ein Fehler bei der Ermittlung des MySQL-Ini-Pfades aufgetreten: $($_.Exception.Message)"  
    }
}



function Get-MySqlDatadir {
    [CmdletBinding()]
    param (
        [String]$MySqlServiceName='MySql'
    )
    try {
        $MySqlService = Get-WmiObject win32_service | Where-Object Name -eq $MySqlServiceName
        if ($MySqlService) {
            [string]$MySqlMyIni = $MySqlService.PathName -replace '.*--defaults-file="?(.*\.ini).*', "`$1"
        } else {
            Throw "Der Dienst `"$MySqlServiceName`" ist kein installierter Dienst:"
        }

        if (!($MySqlmyIni) -or !(Test-Path -Path "$MySqlmyIni" -ErrorAction SilentlyContinue)) {
            Throw "Die MySql Konfigurationsdatei `"$MySqlMyIni`" existiert nicht."
        } else {
            # Method 1: get datadir from my.ini
            Write-Verbose "Ermittlung des MySql-Data-Verzeichnisses aus der my.ini"
            [string]$MySqlDatadir = ((((Get-Content $MySqlMyIni -ErrorAction SilentlyContinue | Select-String -Pattern "datadir=") -replace '[\r\n]') -replace "datadir=") -replace "/","\").Trim("`"")
        }

        if (!($MySqlDatadir)) {
            Write-Verbose "Ermittlung des MySql-Data-Verzeichnisses aus dem Basispfad"
            # Method 2: get datadir by guessing that it is in the mysql basedir
            $MySqlDatadir = $MySqlService.PathName -replace '^"?(.*\\)bin\\mysqld.*', '$1data\'
        } 

        if (!($MySqlDatadir) -or !(Test-Path -Path "$MySqlDatadir" -ErrorAction SilentlyContinue)) {
            Write-Warning "Das MySql-Data-Verzeichnis konnte nicht ermittelt werden."
        } else {
            $MySqlDatadir.TrimEnd("\")
        }
    }
    catch {
        Write-Error "Es ist ein Fehler bei der Ermittlung des MySQL-Data-Pfades aufgetreten: $($_.Exception.Message)"  
    }
}



function Get-MySqlVariables {
    [CmdletBinding()]
    param (
        [string]$VariableName,
        [string]$MySqlServiceName='MySql',
        [string]$MySqlHostname='localhost',
        [string]$MySqlUser='root',
        [string]$MySqlPass=''
    )
    try {
        Clear-Variable MySqlVariables -ErrorAction SilentlyContinue
        $QueryResult = (Invoke-MySqlQuery -MySqlQuery "SHOW VARIABLES" -MySqlUser $MySqlUser -MySqlPass $MySqlPass) -replace '\t', '='
        if ($QueryResult) {
            $QueryResult | ForEach-Object {
                $MySqlVariables += ConvertFrom-StringData -StringData ($_ -replace '[\r\n]')
            }
        }
        
        if ($VariableName) {
            $MySqlVariables.$VariableName
        } else {
            $MySqlVariables
        }
    }
    catch {
        Write-Error "Es ist ein Fehler bei der Abfrage der MySql-Variablen aufgetreten: $($_.Exception.Message)"
    }
}



function Get-GreyhoundInstallPath {
    try {
        $GreyhoundInstallPath = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Greyhound' -Name InstallLocation -ErrorAction SilentlyContinue).InstallLocation
        if (($GreyhoundInstallPath) -and (Test-Path $GreyhoundInstallPath)) {
            $GreyhoundInstallPath
        } elseif (($GreyhoundInstallPath) -and !(Test-Path $GreyhoundInstallPath)) {
            Throw "Der in der Registrierung hinterlegte GREYHOUND Installationspfad `"$GreyhoundInstallPath`" existiert nicht."
        } else {
            Write-Verbose "GREYHOUND ist nicht installiert."
            return $null
        }
    }
    catch {
        Write-Error "Es ist ein Fehler bei der Ermittlung des GREYHOUND Installationspfades aufgetreten: $($_.Exception.Message)"
    }
}



<#
.SYNOPSIS
    Ermittelt die Version der GREYHOUND-Installation
.DESCRIPTION
    Ermittelt die Version der GREYHOUND-Installation
.EXAMPLE
    Example of how to use this cmdlet
#>

function Get-GreyhoundVersionInfo {
    try {
        $GreyhoundServerExe = (Get-GreyhoundInstallPath) + 'Server\GreyhoundServer.exe'
        if (Test-Path -Path $GreyhoundServerExe) {
            $Version = (Get-Item $GreyhoundServerExe).VersionInfo.ProductVersionRaw
            $Version
        } else {
            Throw "GREYHOUND ist nicht installiert"
        }   
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



function Invoke-GreyhoundAdmin {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [Switch]$Start,
        [Switch]$Stop
    )
   
    try {
        $GreyhoundAdmin = (Get-GreyhoundInstallPath) + 'Server\GreyhoundAdmin.exe'
        if (!(Test-Path $GreyhoundAdmin)) {
            Throw "Der GREYHOUND Admin wurde nicht gefunden"
        }

        if ($Start) {
            if ((Start-Process -FilePath "$GreyhoundAdmin" -ArgumentList "-Start -NoGui" -Wait -NoNewWindow -PassThru).Exitcode -gt 0) {
                Throw "Der GREYHOUND Admin hat einen ExitCode ausgegeben"
            }
        } elseif ($Stop) {
            if ((Start-Process -FilePath "$GreyhoundAdmin" -ArgumentList "-Stop -NoGui" -Wait -NoNewWindow -PassThru).Exitcode -gt 0) {
                Throw "Der GREYHOUND Admin hat einen ExitCode ausgegeben"
            }
        }

    }
    catch {
        Write-Error $_.Exception.Message
    }
}



function Restart-GreyhoundServer {
    try {
        Invoke-GreyhoundAdmin -Stop
        Invoke-GreyhoundAdmin -Start           
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



function Get-GreyhoundSystemPassword {
    try {
        $GreyhoundServerIni = (Get-GreyhoundInstallPath) + 'Server\GreyhoundServer.ini'
        if (Test-Path $GreyhoundServerIni) {
            $GreyhoundSystemPassword = (Get-Content $GreyhoundServerIni | Select-String -Pattern 'SystemPassword' -SimpleMatch | ConvertFrom-StringData).SystemPassword
            $GreyhoundSystemPassword
        } else {
            Write-Verbose "Die GREYHOUND Serverkonfiguration `"$GreyhoundServerIni`" wurde nicht gefunden."
        }           
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



<#
.SYNOPSIS
    Zeigt die Einstellungen des GREYHOUND-Servers an
.DESCRIPTION
    Ohne Parameter liefert dieses Cmdlet alle Werte der GreyhoundServer.ini. Wird nur ein bestimmter Wert benötigt, so kann dieser explizit ueber den Key angegeben werden.
#>

function Get-GreyhoundServerIniValue {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [String]$Key,
        [Parameter(ParameterSetName='MySQL')]
        [String]$Compression,
        [ValidateSet('MySQL', 'Global', 'LogFile', 'HtmlInline', 'IndexServer', 'AppServer', 'QueueServer',
            'AntiSpam', 'OCR', 'AccessServer', 'DataExchangeServer', 'SyncServer', 'AddOnServer', 'ItemCount',
            'CommServer', 'AutoClassificationServer')][String]$Section
    )

    try {
        $GreyhoundServerIni = (Get-GreyhoundInstallPath) + 'Server\GreyhoundServer.ini'
        if (Test-Path $GreyhoundServerIni) {
            if (!$Key) {
                $Result = Get-Content $GreyhoundServerIni
            } else {
                $Result = (Get-Content $GreyhoundServerIni | Select-String "^$Key=" | ConvertFrom-StringData).$Key
            }
            $Result
        } else {
            Write-Verbose "Die GREYHOUND Serverkonfiguration `"$GreyhoundServerIni`" wurde nicht gefunden."
        }
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



function Get-GreyhoundFiles {
    [CmdletBinding()]
    [Alias("Get-GreyhoundSetup", "Get-GreyhoundServerSetup", "Get-MariaDBSetup")]
    param (
        [string]$DownloadDir=$PWD.Path,
        [string]$BaseUrl = 'https://greyhound-software.com/files/greyhound',
        [ValidateSet('GreyhoundSetup', 'GreyhoundAspSetup', 'MariaDBSetup')]
        [string]$Component = 'GreyhoundSetup',
        [ValidateSet('Stable', 'Beta', 'Test')]
        [string]$Edition = 'Stable',
        # the following two switches are deprecated. Use "Edition" parameter instead.
        [switch]$Beta,
        [switch]$Test
    )

    if ($Beta) {
        $Edition = "Beta"
    } elseif ($Test) {
        $Edition = "Test"
    }

    try {
        switch -Wildcard ($Component) {
            'Greyhound*' {
                switch ($Edition) {
                    'Stable' {$SetupName = "${Component}.exe"}
                    'Beta' {$SetupName = "${Component}Beta.exe"}
                    'Test' {$SetupName = "${Component}Test.exe"}    
                }
            }
            'MariaDBSetup' {
                $SetupName = "${Component}.msi"
            }
        }
    
        $RemoteFile = "$BaseUrl/$SetupName"
        $LocalFile = $DownloadDir.TrimEnd('\') + '\' + $SetupName
    
        if (Test-Path $LocalFile) {
            [Int64]$RemoteFileSize = (Invoke-WebRequest -Uri $RemoteFile -Method Head -UseBasicParsing).Headers.'Content-Length'
            [Int64]$LocalFileSize = (Get-Item $LocalFile).Length

            Write-Verbose "LocalFileSize: $LocalFileSize RemoteFileSize: $RemoteFileSize"
   
            if ($RemoteFileSize -gt 0 -and $LocalFileSize -gt 0) {
                if ($RemoteFileSize -ne $LocalFileSize) {
                    Start-BitsTransfer -Source $RemoteFile -Destination $LocalFile -Description "Downloading $RemoteFile"
                } else {
                    Write-Verbose "Die Datei `"$SetupName`" mit einer Dateigroesse von $RemoteFileSize Bytes existiert bereits."
                }
            } else {
                Throw "Es ist ein Fehler beim Dateigroessenvergleich aufgetreten."
            }
        } else {
            Start-BitsTransfer -Source $RemoteFile -Destination $LocalFile -Description "Downloading $RemoteFile"
        }
    
        if (Test-Path $LocalFile) {
            $LocalFile
        } else {
            Throw "Es ist ein Fehler beim Herunterladen der Datei '$RemoteFile' aufgetreten."
        }
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



function Get-WindowsServerFiles {
    [CmdletBinding()]
    param (
        [string]$DownloadDir=$PWD.Path,
        [string]$BaseUrl = 'https://greyhound-software.com/files/greyhound/tools/WindowsServerSetup',
        [string]$FileListUrl = "$BaseUrl/Filelist.txt"
        )
    try {
        $FileList = (Invoke-WebRequest -URI "$FileListUrl" -UseBasicParsing).Content -split "`r`n|`n" | ConvertFrom-String -Delimiter '=' -PropertyNames 'Type', 'Source'

        for ($i=0; $i -le $FileList.Length -1; $i++) {
            $RemoteUri = "$BaseUrl/$($FileList[$i].Source)"
            Get-RemoteFile -RemoteUri $RemoteUri -DownloadDir $DownloadDir
        }
    }
    catch {
        Write-Error "Es ist ein Fehler beim Herunterladen der Windows Server Dateien aufgetreten: $($PSItem.ToString())"
    }
}



function Get-RemoteFile {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]$RemoteUri,
        [string]$DownloadDir=$env:TEMP
    )
    function SameFileExists (
        [string]$LocalFile,
        [string]$RemoteFile
    ) {
            
        try {
            if (Test-Path $LocalFile) {
                [Int64]$RemoteFileSize = (Invoke-WebRequest -Uri $RemoteFile -Method Head -UseBasicParsing).Headers.'Content-Length'
                [Int64]$LocalFileSize = (Get-Item $LocalFile).Length

                if ($RemoteFileSize -gt 0 -and $LocalFileSize -gt 0) {
                    if ($RemoteFileSize -ne $LocalFileSize) {
                        Return $false
                    } else {
                        Return $true
                    }
                } else {
                    Throw "Die Dateigroessen konnten nicht ermittelt werden."
                }
            } else {
                Return $false
            }
        }
        catch {
            Write-Error "Es ist ein Fehler beim Dateigroessenvergleich aufgetreten: $PSItem"
        }
    }

    try {
        [string]$LocalFile = "$DownloadDir\" + $RemoteUri.Substring($RemoteUri.LastIndexOf('/') + 1)

        if (!(SameFileExists $LocalFile $RemoteUri)) {
            $Download = Get-BitsTransfer | Where-Object {$_.FileList.RemoteName -eq $RemoteUri}

            if (!($Download)) {
                $Download = Start-BitsTransfer -DisplayName "GREYHOUND Dateitransfer" -Source $RemoteUri -Destination $LocalFile -Asynchronous
            }

            $Status = "Von `"" + $Download.FileList.RemoteName + "`" nach `"" + $Download.FileList.LocalName + "`""
            while ($Download.BytesTransferred -lt $Download.BytesTotal -and $Download.InternalErrorCode -eq 0) {
                Write-Progress -Activity "Die Datei wird heruntergeladen" -Status $Status -PercentComplete ($Download.BytesTransferred/$Download.BytesTotal*100)
            }

            if ($Download.InternalErrorCode -eq 0) {
                $Download.FileList.LocalName
                Complete-BitsTransfer -BitsJob $Download.JobId
            } else {
                Throw "Start-Bitstransfer hat einen Fehler zurueckgegeben"
            }
        } else {
            Write-Verbose "Die Datei `"$LocalFile`" existiert bereits"
            $LocalFile
        }
    }
    catch {
        Write-Error "Es ist ein Fehler beim Herunterladen der Datei `"$RemoteUri`" aufgetreten: $PSItem"
    }
}



function New-GreyhoundVhdx {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]$WindowsIso,
        [string]$UnattendXml,
        [Parameter(Mandatory=$false, ValueFromPipeline=$true)]
        [string]$VhdBasename="GHSRV",
        [string]$VhdPath="$($env:TEMP)\$($VhdBasename).vhdx",
        [string]$WindowsEdition='Windows Server 2019 Standard Core',
        [string]$DiskLayout='UEFI',
        [switch]$DeleteSourceFiles=$false,
        [ValidateRange(12GB, 1TB)]
        [UInt64]$SizeBytes=12GB,
        [switch]$UseDynamicVhd,
        [switch]$Force
    ) 

    try {
        if (!(Test-Path $VhdPath) -or $Force) {
            switch ($WindowsEdition) {
                'Windows Server 2019 Standard Core' {$Edition = '1'}   
                'Windows Server 2019 Standard Gui' {$Edition = '2'}
            }

            if (!($WindowsEdition)) {
                Throw "Es wurde keine gueltige Windows Edition angegeben"
            }

            Convert-WindowsImage -SourcePath $WindowsIso -DiskLayout $DiskLayout -VhdPath $VhdPath -Edition $Edition -UnattendPath $UnattendXml -SizeBytes $SizeBytes -ErrorAction Stop | Out-Null
            if (!($UseDynamicVhd) -and ($VhdPath) -and (Test-Path $VhdPath)) {
                $VhdItem = Get-Item -Path $VhdPath
                $VhdPathFixed = $VhdItem.DirectoryName + '\' + $VhdItem.BaseName + '_fixed' + $VhdItem.Extension
                try {
                    Convert-VHD -Path $VhdPath -DestinationPath $VhdPathFixed -DeleteSource -VHDType Fixed -ErrorAction Stop
                    Rename-Item -Path $VhdPathFixed -NewName $VhdPath -ErrorAction Stop
                }
                catch {
                    Remove-Item $VhdPathFixed -ErrorAction SilentlyContinue
                }
            }

            if ($DeleteSourceFiles) {
                Remove-Item $WindowsIso, $UnattendXml
            }
        }
        Get-VHD $VhdPath -ErrorAction Stop
    }
    catch {
        Write-Error "Es ist ein Fehler beim Erstellen der VHDX-Datei aufgetreten: $PSItem"
    }
}




<#
.SYNOPSIS
    Erstellt eine neue VM fuer die Einrichtung einer neuen GREYHOUND Installation
.DESCRIPTION
    Erstellt eine neue VM fuer die Einrichtung einer neuen GREYHOUND Installation. Dabei wird der erste verfügbare VMSwitch vom Typ "Extern" verwendet. Optional kann eine bereits bestehende VHD angegeben werden. Fehlt dieser Angabe, dann wird eine neue VHD erzeugt.
    Nach der Erstellung werden die Daten der VM in ein eigenes Verzeichnis unterhalb des Standard-Pfades des VMHost verschoben, sodass alle Dateien beisammen sind.
#>

function New-GreyhoundVM {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [String]$VmName,
        [String]$VhdPath,

        [Parameter(Mandatory=$false, ValueFromPipeline=$true)]
        [Int]$VmGeneration=2,
        [Int]$VMProcessorCount=4,

        [ValidateRange(4GB, 16GB)]
        [Int64]$MemoryStartupBytes,

        [switch]$UseDynamicMemory,

        [ValidateRange(4GB, 16GB)]
        [Int64]$MemoryMinimumBytes,

        [ValidateRange(4GB, 16GB)]
        [Int64]$MemoryMaximumBytes,

        [String]$VmNotes='GREYHOUND Server',
        [Microsoft.HyperV.PowerShell.VMSwitch]$VmSwitch,
        [String]$VmBasePath= (Get-VMHost).VirtualMachinePath + '\' + $VmName
    )

    try {
        if (!($VMSwitch)) {
            $AllSwitches = (Get-VMSwitch | Where-Object {$_.SwitchType -eq 'External' -and (Get-NetAdapter -Name ("vEthernet (" + $_.Name + ")") | Where-Object Status -eq 'Up')})
            if ($AllSwitches.Count -eq 1) {
                $VmSwitch = $AllSwitches[0]
            }
        }

        if (Get-VM -Name $VmName -ErrorAction SilentlyContinue) {
            Throw "Eine VM mit dem Namen $VmName existiert bereits"
        }

        if (($VhdPath) -and (Test-Path $VHDPath -ErrorAction SilentlyContinue)) {
            if ($VmSwitch.Count -eq 1 ) {
                $Vm = New-VM -Name $VmName -Generation $VmGeneration -VHDPath $VHDPath -SwitchName $VmSwitch.Name 
            } else {
                $Vm = New-VM -Name $VmName -Generation $VmGeneration -VHDPath $VHDPath
            }

            if ($UseDynamicMemory) {
                Set-VM -VM $Vm -ProcessorCount $VMProcessorCount -DynamicMemory -MemoryStartupBytes $MemoryStartupBytes -MemoryMinimumBytes $MemoryMinimumBytes -MemoryMaximumBytes $MemoryMaximumBytes -Notes $VmNotes -ErrorAction Stop
            } else {
                Set-VM -VM $Vm -ProcessorCount $VMProcessorCount -MemoryStartupBytes $MemoryStartupBytes -Notes $VmNotes -ErrorAction Stop
                Set-VMMemory -VM $Vm -DynamicMemoryEnabled $false
            }

            Move-VMStorage -VM $Vm -DestinationStoragePath $VmBasePath -ErrorAction Stop
            $Vm
        } else {
            Throw "Der Pfad zur VHD-Datei ist ungueltig."
        }
    }
    catch {
        Write-Error "Es ist ein Fehler bei der Erstellung der virtuellen Maschine aufgetreten: $PSItem"
    }
}



<#
.SYNOPSIS
    Installiert MariaDB in einer Silent-Installation
.DESCRIPTION
    Installiert MariaDB in einer Silent-Installation. Die Setupdatei kann auch via Pipeline in Kombination mit Get-MariaDBSetup verwendet werden.
.EXAMPLE
    Get-MariaDBSetup | Install-MariaDBSetup -Password 'MyRootPassword' -InstallDir 'C:\VS\MariaDB'
#>

function Install-MariaDB {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true,
        ValueFromPipeline=$true)]
        [String]$SetupFile,
        [Parameter(Mandatory=$false)]
        [String]$Name='MySql',
        [Switch]$AllowRemoteRootAccess,
        [Boolean]$SkipNetworking,
        [String]$Password,
        [Switch]$NoServiceInstall,
        [String]$InstallDir,
        [Switch]$Log
    )

    $SetupFile = Resolve-Path($SetupFile)
    if (!(Test-Path -Path $SetupFile)) {
        Write-Error "Die Datei '$SetupFile' wurde nicht gefunden." -Category ObjectNotFound
        Break
    }

    $Command = (Get-Command 'msiexec').Path
    $Arguments = @(
        "/package",
        "`"$SetupFile`"",
        "/qn"
    )

    if ($Log) {
        $Arguments += @("/log `"$env:TEMP\MariaDBSetup.log`"")
    }

    if ($Password) {
        $Arguments += @("PASSWORD=$Password")
    }

    if (!($NoServiceInstall)) {
        $Arguments += @("SERVICENAME=$Name")
    }

    if ($InstallDir) {
        $Arguments += @("INSTALLDIR=`"$InstallDir`"")
    }

    if ($AllowRemoteRootAccess) {
        $Arguments += @("ALLOWREMOTEROOTACCESS=True")
    } else {
        $Arguments += @("ALLOWREMOTEROOTACCESS=False")
    }

    Write-Host "Die Datei $SetupFile wird installiert..."

    try {
        $CommandBasename = $Command.Substring($Command.LastIndexOf('\') + 1, $Command.LastIndexOf('.') - $Command.LastIndexOf('\') - 1)
        $StdOutFile = "$env:TEMP\$CommandBasename.stdout"
        $StdErrFile = "$env:TEMP\$CommandBasename.stderr"

        if ((Start-Process -FilePath "$Command" -ArgumentList "$Arguments" -NoNewWindow -RedirectStandardOutput $StdOutFile -RedirectStandardError $StdErrFile -Wait -PassThru).ExitCode -eq 0) {
            Get-Content $StdOutFile
        } else {
            (Get-Content $StdErrFile)
            Throw "Es ist ein Fehler bei der Installation aufgetreten. StdOut: " + (Get-Content $StdErrFile)
        }
    } catch {
        Write-Error "Es ist ein Fehler bei der Ausfuehrung des Befehls '$Command $Arguments' aufgetreten: $PSItem"
    }
    finally {
        Remove-Item $StdOutFile -Force -ErrorAction SilentlyContinue
        Remove-Item $StdErrFile -Force -ErrorAction SilentlyContinue 
    }
}



<#
.SYNOPSIS
    Installiert GREYHOUND in einer Silent-Installation
.DESCRIPTION
    Installiert GREYHOUND in einer Silent-Installation. Die Setupdatei kann auch via Pipeline in Kombination mit Get-GreyhoundServerSetup verwendet werden. Saemtliche Optionen des GUI-Setups können in diesem Cmdlet als Parameter übergeben werden.
    Für eine Serverinstallation sind die Angaben einer Vertragsnummer und einer Seriennummer obligatorisch.
.EXAMPLE
    GreyhoundServerSetup | Install-GreyhoundServer
#>

function Install-GreyhoundServer {
    [CmdletBinding()]
    [Alias("Update-GreyhoundServer")]
    param (
        [Parameter(Mandatory=$true,
        ValueFromPipeline=$true)]
        [String]$SetupFile,
        [Parameter(Mandatory=$false)]
        [String]$ContractNumber,
        [String]$SerialNumber,
        [ValidateSet('Complete', 'Server', 'Client')][String]$Kind='Server',
        [String]$InstallDir="${env:ProgramFiles(x86)}\GREYHOUND\",
        [Switch]$DesktopShortcut=$false,
        [Switch]$StartMenuShortcut=$false,
        [Switch]$QuicklaunchShortcut=$false,
        [Switch]$DefaultMailClient=$false,
        [Switch]$PrinterDriver=$false,
        [Switch]$NoStart=$false,
        [String]$DatabaseUser='root',
        [String]$DatabasePass,
        [int16]$DatabasePort=3306,
        [ValidateSet('Small', 'Medium', 'Large')][String]$DatabaseTemlate='Large',
        [String]$AdminPassword='admin',
        [Switch]$Force=$false
    )

    try {
        $SetupMode = ''

        $SetupFile = Resolve-Path($SetupFile)
        if (Test-Path -Path $SetupFile) {
            $SetupVersion = (Get-Item $SetupFile).VersionInfo.ProductVersionRaw
            $Command = $SetupFile
        } else {
            Write-Error "Die Datei '$SetupFile' wurde nicht gefunden." -Category ObjectNotFound
            Break
        }

        if (!($Force) -and (Get-GreyhoundInstallPath)) {
            $SetupMode = 'Update'
            try {
                $InstalledVersion = Get-GreyhoundVersionInfo
                
                if ($InstalledVersion -gt $SetupVersion) {
                    Write-Host "Die installierte GREYHOUND-Version $InstalledVersion ist neuer als die Installationsdatei $SetupVersion." 
                    if (!($Force)) {
                        Break
                    }
                } elseif ($InstalledVersion -eq $SetupVersion) {
                    Write-Host "GREYHOUND Version $InstalledVersion ist bereits installiert." 
                    if (!($Force)) {
                        Break
                    }
                } else {
                    Write-Verbose "Die Installationsdatei $SetupVersion hat eine neuere Version als die installierte GREYHOUND-Version $InstalledVersion."
                }
    
                [String]$ContractNumber = Get-GreyhoundServerIniValue -Key 'ContractNumber'
                [String]$SerialNumber = Get-GreyhoundServerIniValue -Key 'Serial'

                Write-Verbose "Aktuelle Vertragsnummer: $ContractNumber"
                Write-Verbose "Aktuelle Seriennummer: $SerialNumber"
    
                $Arguments = @(
                    "-silent",
                    "-contract", "`"$ContractNumber`"",
                    "-serial", "`"$SerialNumber`"",
                    "-kind", "$Kind"
                )                  
            }
            catch {
                Write-Error "Es ist ein Fehler bei der Versionsermittlung aufgetreten."
            }
        } else {
            $SetupMode = 'Install'

            $Arguments = @(
                "-silent",
                "-contract", "`"$ContractNumber`"",
                "-serial", "`"$SerialNumber`"",
                "-kind", "$Kind",
                "-targetdir", "`"$TargetDir`"",
                "-databaseuser", "$DatabaseUser",
                "-databaseport", "$DatabasePort"
                "-databasetemplate", "$DatabaseTemlate",
                "-adminpassword", "$AdminPassword"
            )

            if ($DatabasePass) {
                $Arguments += @("-databasepass", "$DatabasePass")
            }
        
            if ($NoStart) {
                $Arguments += @("-nostart")
            }

            if (!$DesktopShortcut) {
                $Arguments += @("-nodesktop")
            }

            if (!$StartMenuShortcut) {
                $Arguments += @("-nostartmenu")
            }

            if (!$QuicklaunchShortcut) {
                $Arguments += @("-noquicklaunch")
            }

            if (!$DefaultMailClient) {
                $Arguments += @("-nodefaultmail")
            }

            if (!$PrinterDriver) {
                $Arguments += @("-noprinterdriver")
            }
        }

        if ($ContractNumber -and $SerialNumber) {
            if ($SetupMode -eq 'Install') {
                Write-Host "GREYHOUND Version $SetupVersion wird installiert..."
                Write-Verbose "Zielverzeichnis: `"$TargetDir`""
            } else {
                $VersionInstalled = (Get-GreyhoundVersionInfo).ToString()
                Write-Host "Die GREYHOUND-Installation wird von Version $VersionInstalled auf Version $SetupVersion aktualisiert..."
            }
            if ((Start-Process -FilePath "$Command" -ArgumentList "$Arguments" -Wait -PassThru).ExitCode -eq 0) {
                Write-Host "Die GREYHOUND-Installation war erfolgreich."
            } else {
                Throw "Das GREYHOUND-Setup hat einen unbekannten Fehler gemeldet."
            }
        } else {
            Write-Error "Fuer eine Installation sind eine Vertrags- und eine Seriennummer notwendig." -Category NotSpecified
            Break
        }
    }
    catch {
        Write-Error "Es ist ein Fehler bei der GREYHOUND-Installation aufgetreten: $($_.Exception.Message)"
    }
}



function Uninstall-GreyhoundServer {
    try {
        $GreyhoundSetupExe = (Get-GreyhoundInstallPath) + 'GreyhoundSetup.exe'
        if (Test-Path -Path $GreyhoundSetupExe) {
            Write-Host "GREYHOUND wird deinstalliert..."
            if ((Start-Process -FilePath "$GreyhoundSetupExe" -ArgumentList '-uninstall -useregistry -silent' -Wait -NoNewWindow -PassThru).ExitCode -eq 0) {
                Write-Host "GREYHOUND wurde erfolgreich deinstalliert."
            } else {
                Throw "Das GREYHOUND-Setup hat einen unbekannten Fehler gemeldet."
            }
        } else {
            Write-Error "Die Datei `"$GreyhoundSetupExe`" ist nicht vorhanden." -Category ObjectNotFound
            Break
        }
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



<#
.SYNOPSIS
    Erstellt einen Dump der GREYHOUND Datenbank
.DESCRIPTION
    Erstellt einen Dump der GREYHOUND Datenbank. Optional kann dieser direkt komprimiert werden.
#>

function New-GreyhoundDatabaseDump {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false,
        ValueFromPipeline=$true)]
        [string]$MySqlUser='root',
        [string]$MySqlPassword,
        [string]$MySqlDatabase='greyhound',
        [switch]$Compress,
        [string]$DestinationPath=$PWD.Path
    )

    $MySqlService = Get-WmiObject win32_service | Where-Object Name -eq 'MySql'
    $MySqlDump = $MySqlService.PathName.Split('mysqld.exe')[0] + 'mysqldump.exe'

    $StdErr = $env:TEMP + '\mysql.stderr'
        
    if (!(Test-Path -Path $MySqlDump)) {
        Write-Warning 'Die Exe-Datei' $MySqlDump 'wurde nicht gefunden.'
        Break
    }

    $MySqlDumpArgs = @(
        "--user=$MySqlUser"
    )

    if ($MySqlPassword) {
        $MySqlDumpArgs += @(
            "--password=$MySqlPassword"
        )
    }
    
    $MySqlDumpArgs += @(
        "--default-character-set=latin1",
        "--max_allowed_packet=128000000"
        "$MySqlDatabase"
    )

    try {
        [string]$SqlFile = $DestinationPath + '\' + $MySqlDatabase + '.sql'
        if ((Start-Process -FilePath $MySqlDump -ArgumentList $MySqlDumpArgs -RedirectStandardOutput $SqlFile -RedirectStandardError $StdErr -Wait -NoNewWindow -PassThru).Exitcode -gt 0) {
            if (Test-Path -Path $StdErr) {
                $ExceptionText = Get-Content $StdErr
                Remove-Item $StdErr
            }
            Throw $ExceptionText
        } else {
            Write-Host $SqlFile
        }
    }
    catch {
        Write-Error $_.Exception.Message
    }
    finally {
        Remove-Item $SqlFile -ErrorAction SilentlyContinue
    }

    if ($Compress) {
        if (Test-Path -Path $SqlFile) {
            $Zip = (Get-Item  $SqlFile).DirectoryName + '\' + (Get-Item  $SqlFile).BaseName + '.zip'
            if (Test-Path -Path $Zip) {
                Remove-Item $Zip
            }
            
            try {
                Write-Verbose "Der Datenbank-Dump wird komprimiert. Ziel: $Zip"
                Compress-Archive -Path $SqlFile -DestinationPath $Zip -CompressionLevel Optimal
            }
            catch {
                Write-Error "Es ist ein Fehler beim Komprimieren des Datenbank-Dumps aufgetreten."
            }
            finally {
                Write-Verbose "Die Datei $SqlFile wird gelöscht."
                Remove-Item $SqlFile 
            }

        } else {
            Write-Warning "Die Datenbank-Datei $SqlFile konnte nicht komprimiert werden, weil sie nicht vorhanden ist."
        }
    }
}



<#
.SYNOPSIS
    Fuert beliebige Sql-Abfragen aus.
.DESCRIPTION
    Dieses Cmdlet ist ein Wrapper für mysql.exe einer bestehenden MySQL- oder MariaDB-Installation. Die gewuenschte Abfrage kann als Parameter eingeschlossen in doppelten Anführungszeichen übergeben werden. Die Ausgabe von mysql.exe wird in der Konsole ausgegeben.
#>

function Invoke-MySqlQuery {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$MySqlQuery,
        [Parameter(Mandatory=$false)]
        [string]$MySqlServiceName='MySql',
        [string]$MySqlHostname='localhost',
        [string]$MySqlUser='root',
        [string]$MySqlPass,
        [string]$Delimiter,
        [switch]$UseRawOutput,
        [switch]$SkipColumnNames
    )

    try {
        $MySqlService = Get-WmiObject win32_service | Where-Object Name -eq $MySqlServiceName

        if (!($MySqlService)) {
            Throw "Der Dienst $MySqlServiceName existiert nicht."
        } elseif ($MySqlService.State -ne 'Running') {
            Throw "Der Dienst $MySqlServiceName ist nicht gestartet."
        }

        $MySqlExe = $MySqlService.PathName -replace '^"?(.*\\).*exe.*', '$1mysql.exe'
        if (!(Test-Path -Path $MySqlExe)) {
            Throw "Die Datei $MySqlExe wurde nicht gefunden."
        }

        $Arguments = @(
            "--host=$MySqlHostname"
            "--user=$MySqlUser"
            '--execute="' + $MySqlQuery + '"'
        )

        if ($MySqlPass) {
            $Arguments += @(
                "--password=$MySqlPass"
            )
        }

        if ($Delimiter) {
            $Arguments += @(
                '--delimiter="' + $Delimiter + '"'
            )
        }

        if ($UseRawOutput) {
            $Arguments += @(
                "--raw"
            )
        }

        if ($SkipColumnNames) {
            $Arguments += @(
                "--skip-column-names"
            )
        }
    
        [String]$StdOut = "$($env:TEMP)\MySql.stdout"
        [String]$StdErr = "$($env:TEMP)\MySql.stderr"

        Start-Process -FilePath "$MySqlExe" -ArgumentList "$Arguments" -NoNewWindow -Wait -RedirectStandardOutput $StdOut -RedirectStandardError $StdErr
        Get-Content $StdOut -ErrorAction SilentlyContinue

        $StdErrContent = Get-Content $StdErr
        if ($StdErrContent) {
            Throw $StdErrContent
        }
        
    } catch {
        Write-Error "Die MySql-Abfrage konnte nicht ausgefuehrt werden: $($_.Exception.Message)"
    } finally {
        Remove-Item $StdOut, $StdErr -Force -ErrorAction SilentlyContinue
    }
}



function Invoke-Slmgr {
    param (
        [CmdletBinding()]
        [string]$Option="/?"
    )

    try {
        $StdOut = "$($env:TEMP)\cscript.stdout"
        $StdErr = "$($env:TEMP)\cscript.stderr"
    
        Start-Process -FilePath "cscript" -ArgumentList "//NoLogo", "$env:SystemRoot\System32\slmgr.vbs", "$Option" -RedirectStandardOutput $StdOut -RedirectStandardError $StdErr -PassThru -Wait -NoNewWindow | Out-Null
        Get-Content $StdOut -Encoding Oem
        Get-Content $StdErr -Encoding Oem
    }
    catch {
        Write-Error "Es ist ein Fehler bei der der Ausfuehrung von slmgr.vbs aufgetreten: $PSItem"
    }
    finally {
        Remove-Item $StdOut -Force -ErrorAction SilentlyContinue
        Remove-Item $StdErr -Force -ErrorAction SilentlyContinue
    }
}



<#
.SYNOPSIS
    Zeigt das letzte Logfile einer GREYHOUND Serverinstalltion an.
.DESCRIPTION
    Zeigt das letzte Logfile einer GREYHOUND Serverinstalltion an.
#>

function Get-GreyhoundLog {
    try {
        $Logfile = Get-ChildItem((Get-GreyhoundInstallPath) + '\Server\Logs\') | Select-Object -Last 1
        Get-Content $Logfile.FullName
    }
    catch {
        Write-Error $_.Exception.Message
    }    
}



<#
.SYNOPSIS
    Loescht die aktuelle GREYHOUND Datenbank
.DESCRIPTION
    Loescht die aktuelle GREYHOUND Datenbank, sodass eine neue, leere Datenbank beim naechsten GREYHOUND Serverstart erstellt wird. Die Dateien im GREYHOUND Data-Verzeichnis werden dabei nicht gelöscht
#>

function Reset-GreyhoundDatabase {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false,
        ValueFromPipeline=$true)]
        [string]$MySqlHostname='localhost',
        [string]$MySqlUser='root',
        [string]$MySqlPass
    )
    try {
        $PrevGreyhoundServerStatus = ((Get-Service GreyhoundService).Status)

        Stop-Service GreyhoundService -ErrorAction Stop
        if (((Get-Service GreyhoundService).Status) -eq 'Stopped') {
            Write-Verbose "Die GREYHOUND Datenbank wird gelöscht..."
            $MySqlQuery = 'DROP DATABASE greyhound;'
            Invoke-MySqlQuery -MySqlQuery $MySqlQuery -MySqlHostname $MySqlHostname -MySqlUser $MySqlUser -MySqlPass $MySqlPass

            Write-Verbose "Die GREYHOUND Dateien werden gelöscht..."
            $ItemsToDelete = Get-ChildItem((Get-GreyhoundInstallPath) + '\Server\') |
                Where-Object Name -In 'AntiSpam', 'Data', 'AutoClassification', 'FulltextIndex', 'GoogleSync', 'ItemCount', 'Logs', 'Share', 'Thumbnails'
            $ItemsToDelete | Remove-Item -Recurse -Force | Out-Null
        } else {
            Throw "Der GREYHOUND-Dienst konnte nicht gestoppt werden."
        }

        if ($PrevGreyhoundServerStatus -eq 'Running') {
            Start-Service GreyhoundService -ErrorAction Stop
            if (((Get-Service GreyhoundService).Status) -ne 'Running') {
                Throw "Der GREYHOUND-Dienst konnte nicht gestartet werden."
            }
        }
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



<#
.SYNOPSIS
    Konfiguriert Windows-Defender-Ausnahmen fuer den GREYHOUND-Serverbetrieb
.DESCRIPTION
    Konfiguriert Windows-Defender-Ausnahmen fuer den GREYHOUND-Serverbetrieb. Vorhandene Einstellungen werden dabei nicht gelöscht.
#>

function Add-GreyhoundDefenderPreference {
    try {
        $GreyhoundInstallPath = (Get-GreyhoundInstallPath).TrimEnd('\')
        $MySqlInstallPath = (Get-MySqlBasedir).TrimEnd('\')

        if ($GreyhoundInstallPath) {
            if ($MySqlInstallPath) {
                Add-MpPreference `
                -ExclusionProcess ("$GreyhoundInstallPath\Server\*", "$GreyhoundInstallPath\Server\Plugins\*", "$MySqlInstallPath\*") `
                -ExclusionPath ("$GreyhoundInstallPath\Server", "$MySqlInstallPath\data")
            } else {
                Throw Der MariaDB-Installationspfad wurde nicht gefunden.
            }
        } else {
            Throw Der GREYHOUND-Installationspfad wurde nicht gefunden.
        }
    } catch {
        Write-Error $_.Exception.Message
    }
}



<#
.SYNOPSIS
    Entfernt eine vorhandene MySql-Tabellenpartitionierung einer GREYHOUND 5-Installation
.DESCRIPTION
    Entfernt eine vorhandene MySql-Tabellenpartitionierung einer GREYHOUND 5-Installation und stellt die notwendigen Indizes wieder her. Dieses Cmdlet macht die Aenderungen von New-GreyhoundDatabasePartitioning wieder rueckgaengig.
#>

function Remove-GreyhoundDatabasePartitioning {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false,
        ValueFromPipeline=$true)]
        [string]$MySqlHostname='localhost',
        [string]$MySqlUser='root',
        [string]$MySqlPass
    )
    try {
        # Auf Partitionierung prüfen
        $Path = (Get-MySqlDatadir) + '\greyhound'
        Write-Verbose "GREYHOUND Datenbank-Pfad: $Path"

        if (($Path) -and (Test-Path $Path) -and (Get-ChildItem -Path $Path -Filter 'items#*')) {
            Write-Verbose "GREYHOUND Dienst wird gestoppt..."
            $PrevGreyhoundServerStatus = ((Get-Service GreyhoundService).Status)

            Stop-Service GreyhoundService -ErrorAction Stop
            if (((Get-Service GreyhoundService).Status) -eq 'Stopped') {
                $MySqlQuery =  @'
USE greyhound;
ALTER TABLE `items` REMOVE PARTITIONING;
ALTER TABLE `items` CHANGE `e_state` `e_state` enum('open','new','question','answer','done','forward','draft','rejected') DEFAULT 'open' NOT NULL;
ALTER TABLE `items` CHANGE `e_kind` `e_kind` enum('email','fax','letter','shortmessage','call','appointment','task','note','contact','file') DEFAULT 'email' NOT NULL;
ALTER TABLE `items` ADD INDEX `e_kind` (`e_kind`);
ALTER TABLE `items` ADD INDEX `e_state` (`e_state`);
ALTER TABLE `items` DROP INDEX `PRIMARY`, ADD PRIMARY KEY(`i_id`);
'@

                Write-Verbose "Die MySQL-Abfrage wird ausgefuehrt: $MySqlQuery"
                Invoke-MySqlQuery -MySqlQuery $MySqlQuery -MySqlHostname $MySqlHostname -MySqlUser $MySqlUser -MySqlPass $MySqlPass -Verbose
            } else {
                Throw "Der GREYHOUND-Dienst konnte nicht gestoppt werden."
            }

            if ($PrevGreyhoundServerStatus -eq 'Running') {
                Write-Verbose "GREYHOUND Dienst wird gestartet..."
                Start-Service GreyhoundService -ErrorAction Stop
                if (((Get-Service GreyhoundService).Status) -ne 'Running') {
                    Throw "Der GREYHOUND-Dienst konnte nicht gestartet werden."
                }
            }
        } else {
            Write-Host "Die GREYHOUND-Datenbank ist nicht partitioniert. Keine Aktion erforderlich."
        }
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



function New-GreyhoundDatabasePartitioning {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false,
        ValueFromPipeline=$true)]
        [string]$MySqlHostname='localhost',
        [string]$MySqlUser='root',
        [string]$MySqlPass
    )
    try {
        # Auf Partitionierung prüfen
        $Path = (Get-MySqlDatadir) + '\greyhound'
        Write-Verbose "GREYHOUND Datenbank-Pfad: $Path" -ErrorAction SilentlyContinue

        if (($Path) -and (Test-Path $Path) -and (!(Get-ChildItem -Path $Path -Filter 'items#*'))) {
            Write-Verbose "GREYHOUND Dienst wird gestoppt..."
            $PrevGreyhoundServerStatus = ((Get-Service GreyhoundService).Status)

            Stop-Service GreyhoundService -ErrorAction Stop
            if (((Get-Service GreyhoundService).Status) -eq 'Stopped') {
                $MySqlQuery =  @'
USE greyhound;
ALTER TABLE `items` DROP INDEX `e_kind`, DROP INDEX `e_state`;
ALTER TABLE `items` CHANGE `e_state` `e_state` TINYINT(1) DEFAULT '1' NOT NULL;
ALTER TABLE `items` CHANGE `e_kind` `e_kind` TINYINT(2) DEFAULT '1' NOT NULL;
ALTER TABLE `items` DROP PRIMARY KEY, ADD PRIMARY KEY (`i_id`, `e_state`, `e_kind`);
ALTER TABLE `items`
PARTITION BY LIST(`e_state`)
SUBPARTITION BY HASH(`e_kind`)
SUBPARTITIONS 10 (PARTITION `isOpen` VALUES IN (1), PARTITION `isNew` VALUES IN (2), PARTITION `isQuestion` VALUES IN (3), PARTITION `isAnswer` VALUES IN (4), PARTITION `isDone` VALUES IN (5), PARTITION `isForward` VALUES IN (6), PARTITION `isDraft` VALUES IN (7), PARTITION `isRejected` VALUES IN (8)
);
'@

                Write-Verbose "Die MySQL-Abfrage wird ausgefuehrt: $MySqlQuery"
                Invoke-MySqlQuery -MySqlQuery $MySqlQuery -MySqlHostname $MySqlHostname -MySqlUser $MySqlUser -MySqlPass $MySqlPass -Verbose
            } else {
                Throw "Der GREYHOUND-Dienst konnte nicht gestoppt werden."
            }

            if ($PrevGreyhoundServerStatus -eq 'Running') {
                Write-Verbose "GREYHOUND Dienst wird gestartet..."
                Start-Service GreyhoundService -ErrorAction Stop
                if (((Get-Service GreyhoundService).Status) -ne 'Running') {
                    Throw "Der GREYHOUND-Dienst konnte nicht gestartet werden."
                }
            }
        } else {
            Write-Host "Die GREYHOUND-Datenbank ist bereits partitioniert. Keine Aktion erforderlich."
        }
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



function Get-ApplianceData {
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory=$true,
            ValueFromPipeline=$true)]
        [string[]]$Serials,
        [Parameter(
            Mandatory=$false)]
        [string]$ApplianceKey
    )

    try {
        $GccUri         = 'https://greyhound-software.com/gcc/jsonrpc/'
        $ContentType    = 'application/json'
    
        $SerialsJson = $Serials | ConvertTo-Json
    
        $Body = @"
{
    "jsonrpc": "2.0",
    "method": "GetApplianceData",
    "params": [
        $SerialsJson,
        "$ApplianceKey"
    ],
    "id": 1
}
"@

        $GccResult = Invoke-WebRequest -Uri $GccUri -Method Post -Body $Body -ContentType $ContentType | ConvertFrom-Json
    
        if ($GccResult.Error) {
            Write-Error $GccResult.Error.Message
        } else {
            $GccResult.Result
        }            
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



function Get-GreyhoundInitialServerData {
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory=$true,
            ValueFromPipeline=$true)]
        [string]$InstallKey
    )

    try {
        $GccUri         = 'https://greyhound-software.com/gcc/jsonrpc/'
        $ContentType    = 'application/json'
    
        $Body = @"
{
    "jsonrpc": "2.0",
    "method": "GetInitialServerData",
    "params": [
        "$InstallKey"
    ],
    "id": 1
}
"@

        $GccResult = Invoke-WebRequest -Uri $GccUri -Method Post -Body $Body -ContentType $ContentType | ConvertFrom-Json
    
        if ($GccResult.Error) {
            Write-Error $GccResult.Error.Message
        } else {
            $GccResult.Result
        }
            
    }
    catch {
        Write-Error $_.Exception.Message
    }
}



<#
.SYNOPSIS
    Migriert sämtliche Daten einer GREYHOUND-Server-Installation von einem Quellsystem
 
.DESCRIPTION
    Migriert sämtliche Daten einer GREYHOUND-Server-Installation oder eines beliebigen Quellverzeichnisses. Dieses Skript ist für die Ausführung auf dem Zielsystem konzipiert.
 
.PARAMETER SourceGhServerPath
    Der Pfad zum GREYHOUND-Server-Verzeichnis auf dem Quellsystem. Beispiel: "C:\VS\GREYHOUND\Server\"
 
.PARAMETER DestinationGhServerPath
    Der Pfad zum GREYHOUND-Server-Verzeichnis auf dem Zielsystem. Beispiel: "C:\VS\GREYHOUND\Server\"
 
.PARAMETER PrepareOnly
    Kopiert nur die GREYHOUND-Server-Dateien. Damit kann eine Migration vorbereitet werden, da der GREYHOUND Server auf dem Quellsystem gestartet bleiben kann.
 
.PARAMETER SourceMariaDbDataPath
    Der Pfad zum MariaDB-Data-Verzeichnis auf dem Quellsystem. Beispiel: "C:\VS\MariaDB\data"
 
.PARAMETER DestinationMariaDbDataPath
    Der Pfad zum MariaDB-Data-Verzeichnis auf dem Zielsystem. Beispiel: "C:\VS\MariaDB\data"
 
.PARAMETER MySqlHostname
    Der Hostname unter dem der MariaDB-Server erreichbar ist. Die Zugangsdaten werden für das automatische Löschen der aktuell installierten Datenbank benötigt. Default: "localhost"
 
.PARAMETER MySqlUser
    Der Benutzername für den Zugriff auf den MariaDB-Server. Die Zugangsdaten werden für das automatische Löschen der aktuell installierten Datenbank benötigt. Default: "root"
 
.PARAMETER MySqlPass
    Das Passwort für den Zugriff auf den MariaDB-Server. Die Zugangsdaten werden für das automatische Löschen der aktuell installierten Datenbank benötigt. Default: "root"
 
.PARAMETER MySqlUser
    Der Benutzername für den Zugriff auf den MariaDB-Server. Die Zugangsdaten werden für das automatische Löschen der aktuell installierten Datenbank benötigt. Default: "root"
 
.PARAMETER SkipAddDefenderPreference
    Standardmäßig werden sowohl bei der Migrationsvorbereitung, als auch bei der Migration, die Defender Ausnahmen für das Zielsystem so konfiguriert, dass keine Virenprüfen beim migrieren der Daten erfolgt. Hiermit kann diese Option abgeschaltet werden.
 
.PARAMETER SkipGreyhoundServerIni
    Standardmäßig wird bei einer Migration die GreyhoundServer.ini übernommen. (Dabei wird der Commserver und der SyncServer deaktiviert). Wenn die aktuelle Konfigurationsdatei beibehalten werden soll, kann diese Option verwendet werden.
 
.PARAMETER RobocopyRetries
    Die Anzahl der Versuche die Robocopy unternimmt, um gesperrte Dateien zu kopieren.
 
.PARAMETER RobocopyWaitBeforeRetry
    Die Wartezeit in Sekunden, bevor Robocopy einen erneuten Kopierversuch bei gesperrten Dateien unternimmt.
 
.PARAMETER RobocopyThreads
    Die Anzahl der Threads mit denen Robocopy Dateien bei der Migrationsvorbereitung kopiert. Bei stark ausgelasteten GREYHOUND-Installationen kann der Wert auf 4-8 reduziert werden.
     
.PARAMETER GhMaxStartupSeconds
    Die Wartezeit in Sekunden, die nach einer Migration auf den Start des GREYHOUND Serverdienstes gewartet wird, bevor eine Fehlermeldung ausgegeben wird. Default: 8 Stunden
 
 
#>

function Copy-GreyhoundFiles {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [String]$SourceGhServerPath,
    
        [Parameter(Mandatory=$true)]
        [String]$DestinationGhServerPath,

        [Parameter(Mandatory=$false, ParameterSetName="Prepare")]
        [Switch]$PrepareOnly,
    
        [Parameter(Mandatory=$true, ParameterSetName="Complete")]
        [String]$SourceMariaDbDataPath,
    
        [Parameter(Mandatory=$true, ParameterSetName="Complete")]
        [String]$DestinationMariaDbDataPath,

        [Parameter(ParameterSetName="Complete")]
        [string]$MySqlHostname='localhost',

        [Parameter(ParameterSetName="Complete")]
        [String]$MySqlUser='root',

        [Parameter(ParameterSetName="Complete")]
        [String]$MySqlPass,

        [Switch]$SkipAddDefenderPreference,
        [Switch]$SkipGreyhoundServerIni,
        
        [Int]$RobocopyRetries=3,
        [Int]$RobocopyWaitBeforeRetry=30,
        [Int]$RobocopyThreads=64,

        [Int]$GhMaxStartupSeconds=8*60*60

    )

    try {
        if ($SourceGhServerPath) {
            $SourceGhServerPath = $SourceGhServerPath.TrimEnd('\')
        }

        if ($DestinationGhServerPath) {
            $DestinationGhServerPath = $DestinationGhServerPath.TrimEnd('\')
        }

        if ($SourceMariaDbDataPath) {
            $SourceMariaDbDataPath = $SourceMariaDbDataPath.TrimEnd('\')
        }

        if ($DestinationMariaDbDataPath) {
            $DestinationMariaDbDataPath = $DestinationMariaDbDataPath.TrimEnd('\')
        }

        Write-Host
        Write-Host "Quell- und Ziel-Verzeichnisse:"
        Write-Host 

        if (!(Test-Path "$SourceGhServerPath\GreyhoundServer.exe" -ErrorAction SilentlyContinue)) {
            Write-Host "Achtung! Der GREYHOUND Quellverzeichnis enthält keine GreyhoundServer.exe. Ist das Verzeichnis korrekt?" -ForegroundColor Red
        } else {
            Write-Host "Quelle: `"$SourceGhServerPath`""
        }

        if (!(Test-Path "$DestinationGhServerPath\GreyhoundServer.exe" -ErrorAction SilentlyContinue)) {
            Write-Host "Achtung! Der GREYHOUND Zielverzeichnis enthält keine GreyhoundServer.exe. Ist das Verzeichnis korrekt?" -ForegroundColor Red
        } else {
            Write-Host "Ziel..: `"$DestinationGhServerPath`""
        }

        if (!($PrepareOnly)) {
            if (!(Test-Path "$SourceMariaDbDataPath\greyhound" -ErrorAction SilentlyContinue)) {
                Write-Host "Achtung! Der MariaDB Quellverzeichnis enthält keine GREYHOUND-Datenbank. Ist das Verzeichnis korrekt?" -ForegroundColor Red
            } else {
                Write-Host "Quelle: `"$SourceMariaDbDataPath`""
            }
        } 

        if (!($PrepareOnly)) {
            if (!(Test-Path "$DestinationMariaDbDataPath\greyhound" -ErrorAction SilentlyContinue)) {
                Write-Host "Achtung! Der MariaDB Zielverzeichnis enthält keine GREYHOUND-Datenbank. Ist das Verzeichnis korrekt?" -ForegroundColor Red
            } else {
                Write-Host "Ziel..: `"$DestinationMariaDbDataPath`""
            }
        }

        Write-Host
        
        if (!($SkipAddDefenderPreference) -and (((Get-MpPreference).ExclusionProcess -match 'GREYHOUND').Count -eq 0)) {
            Write-Host "Windows Defender Ausnahmen werden konfiguriert..."
            try {
                Add-GreyhoundDefenderPreference
            }
            catch {
                Write-Host "Die Defender-Ausnahmen konnten nicht automatisch eingerichtet werden. Möglicherweise ist die Defender-Rolle nicht installiert."              
            }
        }

        $GhRemoveDirs="\ItemCount", "\Logs", "\Temp"
        $GhExclusionDirs="\ItemCount", "\Logs", "\MySQL", "\MariaDB", "\Plugins", "\Temp", "\Updates", "\Web\Unity"
        $GhExclusionFiles="*.dll", "*.exe", "*.bak", "*.txt"
        if ($SkipGreyhoundServerIni) {
            $GhExclusionFiles+="GreyhoundServer.ini"
        }
        
        $MariaDbExclusionFiles="*.err", "*.pid", "*.log", "*.idx", "*.tbl", "*.ini"

        $GreyhoundServiceName = "GreyhoundService"
        $AspServiceName = "FbmAspService"
        $MySqlServiceName = "MySql"
        $MySqlUpgradeExe = (Get-MySqlBasedir) + '\bin\mysql_upgrade.exe'
        $RobocopyExe = [Environment]::SystemDirectory +'\Robocopy.exe'

        if ($PrepareOnly) {
            Write-Host "Die Migrationsvorbereitung synchronisiert alle GREYHOUND-Dateien des Quellsystems mit dem Zielsystem." -ForegroundColor Green
            Write-Host "Das Quellsystem bleibt online. Dieser Vorgangs kann beliebig oft durchgeführt werden." -ForegroundColor Green
            Write-Host
            $Choice = Read-Host "Soll die Migrationsvorbeireitung nun durchgeführt werden? [j/n]"
            if ($Choice -ne "j") {
                Write-Host "Die Migrationsvorbereitung wurde abgebrochen."
                Break
            }

            Write-Host "Migrationsvorbereitung wird durchgeführt..."

            Write-Host "Der Dienst `"$GreyhoundServiceName`" wird auf dem Zielsystem gestoppt..."
            Stop-ServiceEx -Name $GreyhoundServiceName

            Write-Host "Der Dienst `"$AspServiceName`" wird auf dem Zielsystem gestoppt..."
            Stop-ServiceEx -Name $AspServiceName
   
            Write-Host "Überflüssige Verzeichnisse auf dem Zielsystem werden entfernt..."
            $GhRemoveDirs | ForEach-Object {Remove-Item -Path "$DestinationGhServerPath\$_" -Force -Recurse -Confirm:$false -ErrorAction SilentlyContinue}

            Write-Host "Essenzielle GREYHOUND-Dateien werden kopiert..."
            $RobocopyArguments = "`"$SourceGhServerPath`"", "`"$DestinationGhServerPath`"", "/MIR", "/R:$RobocopyRetries", "/W:$RobocopyWaitBeforeRetry", "/MT:$RobocopyThreads", "/XD $($GhExclusionDirs.ForEach({'"' + "$SourceGhServerPath$_" + '"'}))", "/XF $GhExclusionFiles" 
            Start-Process -FilePath "$RobocopyExe" -ArgumentList "$RobocopyArguments" -NoNewWindow -Wait

            Write-Host "Die Migrationsvorbereitung ist abgeschlossen." -ForegroundColor Green
            Write-Host

        } else {
            Write-Host "Achtung! Für eine Migration müssen folgende Voraussetzungen erfüllt sein:" -ForegroundColor Yellow
            Write-Host " - GREYHOUND und MariaDB müssen auf dem Zielsystem installiert sein."
            Write-Host " - Die GREYHOUND Version auf dem Zielsystem muss gleich oder neuer mit der des Quellsystems sein."
            Write-Host " - Auf dem Quellsystem müssen die Dienste MySQL, GreyhoundServer und FbmAspService gestoppt und deaktiviert sein."
            Write-Host " - Eine GREYHOUND 4 Datenbank darf nicht partitioniert sein."
            Write-Host
            $Choice = Read-Host "Sind die Voraussetzungen erfüllt? [j/n]"
            if ($Choice -ne "j") {
                Write-Host "Die Migration wurde abgebrochen."
                Break
            }

            $Choice = Read-Host "Soll die Migration nun durchgeführt werden? [J/N]"
            if ($Choice -ne "J") {
                Write-Host "Die Migration wurde abgebrochen."
                Break
            }
               
            Write-Host "Die Migration wird durchgeführt..."

            Write-Host "Der Dienst `"$GreyhoundServiceName`" wird auf dem Zielsystem gestoppt..."
            Stop-ServiceEx -Name $GreyhoundServiceName

            Write-Host "Der Dienst `"$AspServiceName`" wird auf dem Zielsystem gestoppt..."
            Stop-ServiceEx -Name $AspServiceName

            Write-Host "Überflüssige Verzeichnisse auf dem Zielsystem werden entfernt..."
            $GhRemoveDirs | ForEach-Object {Remove-Item -Path "$DestinationGhServerPath\$_" -Force -Recurse -Confirm:$false -ErrorAction SilentlyContinue}

            Write-Host "Essenzielle GREYHOUND-Dateien werden kopiert..."
            $RobocopyArguments = "`"$SourceGhServerPath`"", "`"$DestinationGhServerPath`"", "/MIR", "/R:$RobocopyRetries", "/W:$RobocopyWaitBeforeRetry", "/MT:$RobocopyThreads", "/XD $($GhExclusionDirs.ForEach({'"' + "$SourceGhServerPath$_" + '"'}))", "/XF $GhExclusionFiles" 
            Start-Process -FilePath "$RobocopyExe" -ArgumentList "$RobocopyArguments" -NoNewWindow -Wait

            Write-Host "Die GREYHOUND Konfigurationsdatei wird mit deaktiviertem Comm- und Syncserver übernommen..."
            (Get-Content -Path $SourceGhServerPath\GreyhoundServer.ini) -replace ('UseCommServer=.*', 'UseCommServer=0') -replace ('UseSyncServer=.*', 'UseSyncServer=0') | Set-Content -Path "$DestinationGhServerPath\GreyhoundServer.ini"

            if (!($SkipAddDefenderPreference)) {
                Write-Host "Windows Defender Ausnahmen werden konfiguriert..."
                try {
                    Add-GreyhoundDefenderPreference
                }
                catch {
                    Write-Host "Die Defender-Ausnahmen konnten nicht automatisch eingerichtet werden. Möglicherweise ist die Defender-Rolle nicht installiert."              
                }
            }

            Write-Host "Die GREYHOUND Datenbank wird gelöscht..."
            $MySqlQuery = 'DROP DATABASE greyhound;'
            Invoke-MySqlQuery -MySqlQuery $MySqlQuery -MySqlHostname $MySqlHostname -MySqlUser $MySqlUser -MySqlPass $MySqlPass -ErrorAction SilentlyContinue

            Write-Host "Der Dienst `"$MySqlServiceName`" wird auf dem Zielsystem gestoppt..."
            Stop-ServiceEx -Name $MySqlServiceName

            Write-Host "MariaDB Data Verzeichnis wird kopiert..."
            $RobocopyArguments = "`"$SourceMariaDbDataPath`"", "`"$DestinationMariaDbDataPath`"", "/MIR", "/R:$RobocopyRetries", "/W:$RobocopyWaitBeforeRetry", "/XF $MariaDbExclusionFiles" 
            Start-Process -FilePath "$RobocopyExe" -ArgumentList "$RobocopyArguments" -NoNewWindow -Wait

            Write-Host "Der Dienst `"$MySqlServiceName`" wird auf dem Zielsystem gestartet..."
            Start-ServiceEx -Name $MySqlServiceName

            Write-Host "MySQL Upgrade wird ausgeführt..."
            
            [String]$StdOut = "$($env:TEMP)\mysql_upgrade.stdout"
            [String]$StdErr = "$($env:TEMP)\mysql_upgrade.stderr"
    
            Start-Process -FilePath "$MySqlUpgradeExe" -NoNewWindow -Wait -RedirectStandardOutput $StdOut -RedirectStandardError $StdErr
            Get-Content $StdOut -ErrorAction SilentlyContinue
    
            $StdErrContent = Get-Content $StdErr
            Remove-Item $StdOut, $StdErr -ErrorAction SilentlyContinue
            if ($StdErrContent) {
                Throw $StdErrContent
            }
    
            Write-Host "Der Dienst `"$GreyhoundServiceName`" wird auf dem Zielsystem gestartet. (Das kann bei großen Systemen bis zu mehreren Stunden dauern.)"
            Start-ServiceEx -Name $GreyhoundServiceName -MaxTimeoutSeconds $GhMaxStartupSeconds

            Write-Host "Der Dienst `"$AspServiceName`" wird auf dem Zielsystem gestartet..."
            Start-ServiceEx -Name $AspServiceName

            Write-Host 
            Write-Host "Die Migration wurde erfolgreich abgeschlossen! Bitte noch an den Comm- und Sync-Server denken :-)" -ForegroundColor Green
            Write-Host
        }
            
    }
    catch {
        Write-Error "Es ist ein Fehler beim Kopieren der Daten aufgetreten: $($_.Exception.Message)"
    }

}