Install-Dynamics365ReportingExtensions.psm1

function Install-Dynamics365ReportingExtensions {
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $MediaDir,
        [string]
        $Patch,
        [string]
        $InstanceName,
        [string]
        $ConfigDBServer,
        [switch]
        $AutoGroupManagementOff,
        [switch]
        $MUOptin = $false,
        [string]
        $LogFilePath = $null,
        [ValidateRange(1,3600)]
        [int]
        $LogFilePullIntervalInSeconds = 30,
        [switch]
        $LogFilePullToOutput = $False
    )
    $AutoGroupManagementOffBool = [bool]$AutoGroupManagementOff;
    $setupFilePath = "$mediaDir\SetupSrsDataConnector.exe";
    $fileVersion = ( Get-Command $setupFilePath ).FileVersionInfo.FileVersionRaw.ToString();
    Write-Output "Version of software to be installed: $fileVersion";
    $fileLanguageCode = ( Get-Item $mediaDir\LangPacks\* ).Name;
    Write-Output "Language code of software to be installed: $fileLanguageCode";
    $Dynamics365Resources | Get-Member -MemberType NoteProperty | Where-Object { $Dynamics365Resources.( $_.Name ).ReportingExtensionsIdentifyingNumber } | ForEach-Object {
        if ( ( $Dynamics365Resources.( $_.Name ).MediaFileVersion -eq $fileVersion ) -and ( $Dynamics365Resources.( $_.Name ).LanguageCode -eq $fileLanguageCode ) ) { $foundFileResource = $_.Name }
    }
    $installedProducts = Get-WmiObject Win32_Product -ComputerName $env:COMPUTERNAME | ForEach-Object { $_.IdentifyingNumber }
    if ( $foundFileResource )
    {
        Write-Output "Found corresponding resource in the catalog: $foundFileResource";
        $expectedProductIdentifyingNumber = $Dynamics365Resources.$foundFileResource.ReportingExtensionsIdentifyingNumber;
    } else {
        Write-Output "Corresponding resource is not found in the catalog. Installation verification will be skipped";
    }
    if ( $expectedProductIdentifyingNumber )
    {
        $isInstalled = $installedProducts -contains "{$expectedProductIdentifyingNumber}";
    } else {
        Write-Output "IdentifyingNumber is not specified for this product, installation verification will be skipped";
    }
    # Starting installation only when not installed or if id is not found in catalog.
    if ( !$expectedProductIdentifyingNumber -or !$isInstalled ) {
        if ( $Patch ) {
            $resourcePathItem = Get-Item $Patch;
            if ( !$resourcePathItem ) { throw "Specified file or directory '$Patch' could not be found" }
            if ( $resourcePathItem.PSIsContainer ) {
                $patchFiles = Get-Item $Patch\Srs_KB*_amd64_$fileLanguageCode.msp
                if ( $patchFiles ) {
                    $patchFilePath = $patchFiles[0].FullName;
                    Write-Output "Found patch file $patchFilePath"
                } else {
                    throw "File could not be found: $Patch\Srs_KB*_amd64_$fileLanguageCode.msp"
                }
            } else {
                $patchFilePath = $Patch;
            }
        }
        $xml = [xml]"";
        $crmSetupElement = $xml.CreateElement( "CRMSetup" );
            $srsDataConnectorElement = $xml.CreateElement( "srsdataconnector" );
                $configDBServerElement = $xml.CreateElement( "configdbserver" );
                    $configDBServerElement.InnerText = $configDBServer;
                $srsDataConnectorElement.AppendChild( $configDBServerElement ) | Out-Null;
                $autoGroupManagementOffElement = $xml.CreateElement( "autogroupmanagementoff" );
                    $autoGroupManagementOffElement.InnerText = [int]$AutoGroupManagementOffBool;
                $srsDataConnectorElement.AppendChild( $autoGroupManagementOffElement ) | Out-Null;
                if ( $InstanceName ) {
                    $instanceNameElement = $xml.CreateElement( "instancename" );
                        $instanceNameElement.InnerText = $InstanceName;
                    $srsDataConnectorElement.AppendChild( $instanceNameElement ) | Out-Null;
                }
                $patchElement = $xml.CreateElement( "Patch" );
                    if ( $null -ne $Patch ) {
                        $patchElement.SetAttribute( "Update", $true ) | Out-Null;
                        $patchElement.InnerText = $patchFilePath;
                    } else {
                        $patchElement.SetAttribute( "Update", $false ) | Out-Null;
                    }
                $srsDataConnectorElement.AppendChild( $patchElement ) | Out-Null;
                $MUOptinElement = $xml.CreateElement( "muoptin" );
                    $MUOptinElement.SetAttribute( "optin", $MUOptin.ToString().ToLower() ) | Out-Null;
                $srsDataConnectorElement.AppendChild( $MUOptinElement ) | Out-Null;
            $crmSetupElement.AppendChild( $srsDataConnectorElement ) | Out-Null;
        $xml.AppendChild( $crmSetupElement ) | Out-Null;

        $stringWriter = New-Object System.IO.StringWriter;
        $xmlWriter = New-Object System.Xml.XmlTextWriter $stringWriter;
        $xmlWriter.Formatting = "indented";
        $xml.WriteTo( $xmlWriter );
        $xmlWriter.Flush();
        $stringWriter.Flush();
        $xmlConfig = $stringWriter.ToString();

        if([String]::IsNullOrEmpty($logFilePath) -eq $True) {
            $timeStamp = ( Get-Date -Format u ).Replace(" ","-").Replace(":","-");
            $logFilePath = "$env:APPDATA\Microsoft\MSCRM\Logs\DynamicsReportingExtensionsInstallationLog_$timeStamp.txt";
        }
        $tempFileName = [guid]::NewGuid().Guid;
        $tempFilePath = "$env:Temp\$tempFileName.xml";
        Write-Output "$(Get-Date) Saving configuration temporary to $tempFilePath";
        Set-Content -Path $tempFilePath -Value $xmlConfig -Encoding utf8;
        Get-Content $tempFilePath;
        Write-Output "$(Get-Date) Starting $setupFilePath";
        $installCrmScript = {
            param( $setupFilePath, $tempFilePath, $logFilePath );
            Write-Output "Start-Process '$setupFilePath' -ArgumentList '/Q /config $tempFilePath /L $logFilePath' -Wait;";
            Start-Process "$setupFilePath" -ArgumentList "/Q /config $tempFilePath /L $logFilePath" -Wait;
        }
        $job = Start-Job -ScriptBlock $installCrmScript -ArgumentList $setupFilePath, $tempFilePath, $logFilePath;
        Write-Output "$(Get-Date) Started installation job, log will be saved in $logFilePath";
        $lastLinesCount = 0;
        $startTime = Get-Date;
        do {
            $elapsedTime = $( Get-Date ) - $startTime;
            $elapsedString = "{0:HH:mm:ss}" -f ( [datetime]$elapsedTime.Ticks );
            Write-Output "$(Get-Date) Elapsed $elapsedString. Waiting until CRM reporting extensions job is done, sleeping $logFilePullIntervalInSeconds sec";
            Start-Sleep $logFilePullIntervalInSeconds;
            $jobState = $job.State;
            if ( $logFilePullToOutput ) {
                if ( Test-Path $logFilePath ) {
                    $logFileContents = Get-Content $logFilePath -ReadCount 0;
                    $linesCount = $logFileContents.Length;
                    $newLinesCount = $linesCount - $lastLinesCount;
                    if($newLinesCount -gt 0) {
                        Write-Output "$(Get-Date) - new logs: $newLinesCount lines";
                        $logFileContents | Select-Object -First $newLinesCount -Skip $lastLinesCount | Write-Output;
                    } else {
                        Write-Output "$(Get-Date) - no new logs";
                    }
                    $lastLinesCount = $linesCount;
                }
            }
        } until ( $jobState -eq "Completed" )
        $elapsedTime = $( Get-Date ) - $startTime;
        $elapsedString = "{0:HH:mm:ss}" -f ( [datetime]$elapsedTime.Ticks );
        Write-Output "$(Get-Date) Elapsed $elapsedString. Job is complete, output:";
        Write-Output ( Receive-Job $job );
        Remove-Job $job;
        Start-Sleep 10;
        Remove-Item $tempFilePath;
        if( (Test-Path $logFilePath) -eq $True) {
            $errorLines = Get-Content $logFilePath | Select-String -Pattern "Error|" -SimpleMatch;
            if ( $errorLines ) {
                Write-Output "Errors from install log:";
                $errorLines | Foreach-Object {
                    Write-Output $_.Line;
                }
            }
        }
        Write-Output "The following products were installed:"
        Get-WmiObject Win32_Product -ComputerName $env:COMPUTERNAME | ForEach-Object {
            if ( !$expectedProductIdentifyingNumber -or ( $_.IdentifyingNumber -eq "{$expectedProductIdentifyingNumber}" ) ) {
                $isInstalled = $true;
            }
            if ( !( $installedProducts -contains $_.IdentifyingNumber ) ) {
                Write-Output $_.IdentifyingNumber, $_.Name;
            }
        }
        if ( $expectedProductIdentifyingNumber )
        {
            if ( $isInstalled ) {
                Write-Output "Installation is finished and verified successfully";
            } else {
                $errorMessage = "Installation job finished but the product $expectedProductIdentifyingNumber is still not installed";

                Write-Output $errorMessage;
                Throw $errorMessage;
            }
        } else {
            Write-Output "Installation is finished but verification cannot be done without IdentifyingNumber specified.";
        }
    } else {
        Write-Output "Product is already installed, skipping";
    }
}