diff --git a/Microsoft.Xrm.Data.PowerShell/Microsoft.Xrm.Data.PowerShell.psm1 b/Microsoft.Xrm.Data.PowerShell/Microsoft.Xrm.Data.PowerShell.psm1 index 3ab2b6f..b30e0fb 100644 --- a/Microsoft.Xrm.Data.PowerShell/Microsoft.Xrm.Data.PowerShell.psm1 +++ b/Microsoft.Xrm.Data.PowerShell/Microsoft.Xrm.Data.PowerShell.psm1 @@ -1970,15 +1970,13 @@ function Import-CrmSolutionAsync { ) $conn = VerifyCrmConnectionParam -conn $conn -pipelineValue ($PSBoundParameters.ContainsKey('conn')) $importId = [guid]::Empty - $asyncResponse = $null try { if (!(Test-Path $SolutionFilePath)) { throw [System.IO.FileNotFoundException] "$SolutionFilePath not found." } - - Write-Verbose "Importing solution file $SolutionFilePath into: $($conn.CrmConnectOrgUriActual)" + Write-Verbose "Importing solution file $SolutionFilePath into: $($conn.CrmConnectOrgUriActual)" Write-Verbose "OverwriteCustomizations: $OverwriteUnManagedCustomizations" Write-Verbose "SkipDependancyCheck: $SkipDependancyOnProductUpdateCheckOnInstall" Write-Verbose "ImportAsHoldingSolution: $ImportAsHoldingSolution" @@ -1995,100 +1993,199 @@ function Import-CrmSolutionAsync { $PublishChanges = $false; } - $data = [System.IO.File]::ReadAllBytes($SolutionFilePath) - - $request = New-Object Microsoft.Crm.Sdk.Messages.ImportSolutionRequest - $request.CustomizationFile = $data - $request.PublishWorkflows = $ActivateWorkflows - $request.OverwriteUnmanagedCustomizations = $OverwriteUnManagedCustomizations - $request.SkipProductUpdateDependencies = $SkipDependancyOnProductUpdateCheckOnInstall - $request.HoldingSolution = $ImportAsHoldingSolution - - $asyncRequest = New-Object Microsoft.Xrm.Sdk.Messages.ExecuteAsyncRequest - $asyncRequest.Request = $request; + Write-Verbose "Importing solution. This process can take a while..." + Write-Progress -Id 1 -Activity "Importing Solution" -CurrentOperation "Initiating import" -PercentComplete -1 + $asyncOperationId = $conn.ImportSolutionToCrmAsync($SolutionFilePath, [ref]$importId, $ActivatePlugIns, + $OverwriteUnManagedCustomizations, $SkipDependancyOnProductUpdateCheckOnInstall,$ImportAsHoldingSolution) + Write-Verbose "ImportId: $importId" + Write-Verbose "AsyncOperationId: $asyncOperationId" + if ($importId -eq [guid]::Empty) { + throw LastCrmConnectorException($conn) + } - Write-Verbose "Importing solution. This process can take a while..." - try - { - $asyncResponse = ($conn.ExecuteCrmOrganizationRequest($asyncRequest, "AsyncImportRequest") -as [Microsoft.Xrm.Sdk.Messages.ExecuteAsyncResponse]) - $importId = $asyncResponse.AsyncJobId - - Write-Verbose "Async Operation ID (asyncoperationid): $importId" - if($importId -eq $null -or $importId -eq [Guid]::Empty) - { - throw "Import request failed for Async Operation {$importId}" - } - #if the caller wants to get the ID and does NOT want to wait - if($BlockUntilImportComplete -eq $false){ - return $asyncResponse; - } - } - catch - { - throw LastCrmConnectorException($conn) - } + if (-not $BlockUntilImportComplete) { + try { + $asyncOperationRecords = (Get-CrmRecords -conn $conn -EntityLogicalName asyncoperation -FilterAttribute asyncoperationid -FilterOperator eq -FilterValue $asyncOperationId -Fields *).CrmRecords + return if ($asyncOperationRecords.Count -gt 0) { $asyncOperationRecords[0] } else { $null } + } catch { + if($transientFailureCount -gt 5){ + Write-Error "Async operation status check FAILED 5 times this could be due to a bug where the service returns a 401. Throwing lastException:"; + throw $conn.LastCrmException + } + Write-Verbose "Async operation status check FAILED this could be due to a bug where the service returns a 401. We'll allow up to 5 failures before aborting."; + $transientFailureCount++; + } + } - $pollingStart = Get-Date $isProcessing = $true - $secondsSpentPolling = 0 + $timeSpentPolling = [System.Diagnostics.Stopwatch]::StartNew() + $timeout = New-TimeSpan -Seconds $MaxWaitTimeInSeconds + $pollingDelaySeconds = 5 + $TopPrevProcPercent = [double]0 + $isProcPercentReduced = $false + #this is for a bug where the service will throw a 401 on retrieve of importjob during an import under certain conditions $transientFailureCount = 0; - Write-Verbose "Solution import requested, waiting on completion of Async Operation {$importId}..." - + Write-Host "Import of file completed, waiting on completion of importId: $importId" try{ - while(($isProcessing -and $secondsSpentPolling -lt $MaxWaitTimeInSeconds) -or ($isProcessing -and $MaxWaitTimeInSeconds -eq -1)){ - #delay - Start-Sleep -Seconds $PollingDelayInSeconds - - #check the import job for success/fail/inProgress - try{ - $import = Get-CrmRecord -conn $conn -EntityLogicalName asyncoperation -Id $importId -Fields statuscode,completedon,friendlymessage + Write-Progress -Id 1 -Activity "Importing Solution" -CurrentOperation "Solution file uploaded" -PercentComplete -1 + while($isProcessing -and $timeSpentPolling.Elapsed -lt $timeout){ + + try { + $asyncOperationRecords = (Get-CrmRecords -conn $conn -EntityLogicalName asyncoperation -FilterAttribute asyncoperationid -FilterOperator eq -FilterValue $asyncOperationId -Fields friendlymessage,statuscode,message).CrmRecords + $asyncOperation = if ($asyncOperationRecords.Count -gt 0) { $asyncOperationRecords[0] } else { $null } } catch { + if($transientFailureCount -gt 5){ + Write-Error "Async operation status check FAILED 5 times this could be due to a bug where the service returns a 401. Throwing lastException:"; + throw $conn.LastCrmException + } + Write-Verbose "Async operation status check FAILED this could be due to a bug where the service returns a 401. We'll allow up to 5 failures before aborting."; $transientFailureCount++; - Write-Verbose "Import Job status check did not succeed: $($_.Exception)" } - $statuscode = $import.statuscode_Property.value.Value; - - #Check for import completion - https://msdn.microsoft.com/en-us/library/gg309288.aspx - if($statuscode -lt 30){ - $secondsSpentPolling = ([Int]((Get-Date) - $pollingStart).TotalSeconds) - Write-Verbose "Executing for $($secondsSPentPolling.ToString("000"))/$MaxWaitTimeInSeconds seconds | Status: $($import.statuscode) ($statuscode)" - } - elseif($statuscode -eq 31 -or $statuscode -eq 32 ){ - $isProcessing = $false - throw "Status: $($import.statuscode) ($statuscode) | Operation has been either cancelled or has failed for Async Operation {$importId}.`n$($import.friendlymessage)" - break; - } - elseif($statuscode -eq 30){ - $isProcessing = $false - Write-Verbose "Processing Completed at: $($import.completedon)" - if($PublishChanges){ - Write-Verbose "SUCCESS: PublishChanges set, executing: Publish-CrmAllCustomization using the same connection." - Publish-CrmAllCustomization -conn $conn - return $import - } - else{ - Write-Verbose "SUCCESS: Import Complete, don't forget to publish customizations." - return $import - } - break; - } - } - - #User provided timeout and exit function with an error - if($secondsSpentPolling -gt $MaxWaitTimeInSeconds){ - Write-Warning "Import-CrmSolutionAsync exited due to exceeding the maximum timeout of $MaxWaitTimeInSeconds. The import will continue in CRM async until it either succeeds or fails." + if ($asyncOperation.statuscode -eq "Failed") { + Write-Error (Coalesce $asyncOperation.friendlyMessage $asyncOperation.message $asyncOperation.statuscode) + throw $asyncOperation.message + } elseif ($asyncOperation.statuscode -eq "Canceled") { + throw "Import cancelled" + } elseif ($asyncOperation.statuscode -ne "In Progress" -and $asyncOperation.statuscode -ne "Succeeded") { + $currentOperation = Coalesce $asyncOperation.friendlyMessage $asyncOperation.statuscode + Write-Progress -Id 1 -Activity "Importing $($import.solutionname)" -CurrentOperation "Import processing - $currentOperation" -PercentComplete -1 + Write-Verbose "Importing $($import.solutionname) - Import processing - $currentOperation" + } else { # In Progress or Succeeded + + try { + $importRecords = (Get-CrmRecords -conn $conn -EntityLogicalName importjob -FilterAttribute importjobid -FilterOperator eq -FilterValue $importId -Fields data,completedon,startedon,progress).CrmRecords + $import = if ($importRecords.Count -gt 0) { $importRecords[0] } else { $null } + } catch { + if($transientFailureCount -gt 5){ + Write-Error "Import Job status check FAILED 5 times this could be due to a bug where the service returns a 401. Throwing lastException:"; + throw $conn.LastCrmException + } + Write-Verbose "Import Job status check FAILED this could be due to a bug where the service returns a 401. We'll allow up to 5 failures before aborting."; + $transientFailureCount++; + } + + + $importManifest = ([xml]($import).data).importexportxml.solutionManifests.solutionManifest + $ProcPercent = [double](Coalesce $import.progress "0") + + Write-Progress -Id 1 -Activity "Importing $($import.solutionname)" -CurrentOperation "Import processing" -PercentComplete $ProcPercent + Write-Verbose "Importing $($import.solutionname) - $($ProcPercent)%" + + #check if processing percentage reduced at any given time + if($TopPrevProcPercent -gt $ProcPercent) + { + $isProcPercentReduced = $true + Write-Verbose "Processing is reversing... import will fail." + } else { + $TopPrevProcPercent = $ProcPercent + } + + #Check for import completion + if($null -eq $import.completedon -and $importManifest.result.result -ne "success"){ + $isProcessing = $true + } + else { + Write-Verbose "Processing Completed at: $($import.completedon) with ImportJob%: $ProcPercent" + Write-Verbose "Import Manifest Result: $($importManifest.result.result) with ImportJob%: $ProcPercent" + + $solutionImportResults = Select-Xml -Xml ([xml]$import.data) -XPath "//result" + $anyFailuresInImport = $false; + $allErrorText = ""; + + foreach($solutionImportResult in $solutionImportResults) + { + try{ + $resultParent = "" + $itemResult = "" + $resultParent = $($solutionImportResult.Node.ParentNode.ParentNode.Name) + $itemResult = $($solutionImportResult.Node.result) + }catch{} + + Write-Verbose "Item:$resultParent result: $itemResult" # write each item result in result data + + if ($solutionImportResult.Node.result -ne "success") + { + # if any error in result print more error details + try{ + $errorCode = "" + $errorText = "" + $moreErrorDetails = "" + $errorCode = $($solutionImportResult.Node.errorcode) + $errorText = $($solutionImportResult.Node.errortext) + $moreErrorDetails = $solutionImportResult.Node.parameters.InnerXml + + }catch{} + + Write-Verbose "errorcode: $errorCode errortext: $errorText more details: $moreErrorDetails" + if ($solutionImportResult.Node.result -eq "failure") # Fail only on errors, not on warnings + { + $anyFailuresInImport = $true; # mark if any failures in solution import + $allErrorText = $allErrorText + ";" + $errorText; + } + } + } + if(-not $isProcPercentReduced -and $importManifest.result.result -eq "success" -and (-not $anyFailuresInImport)) + { + Write-Verbose "Setting to 100% since all results are success" + $ProcPercent = 100.0 + } + $isProcessing = $false + break + } + } + + Write-Progress -Id 2 -ParentId 1 -Activity "Timeout ($timeout)" -CurrentOperation "Remaining Time until operation times out locally: $($timeout - $timeSpentPolling.Elapsed)" -PercentComplete ([double]($timeout - $timeSpentPolling.Elapsed).TotalSeconds * 100 / $timeout.TotalSeconds) + Start-Sleep -Seconds $pollingDelaySeconds } - #at this point we appear to have imported successfully - return $asyncResponse; } Catch { - throw "AsyncOperation with ID: $importId has encountered an exception: $_" + Write-Error "ImportJob with ID: $importId has encountered an exception: $_ " + } Finally{ + $ProcPercent = ([double](Coalesce $ProcPercent 0)) + Write-Progress -Id 2 -Completed -Activity "_" + $timeSpentPolling.Stop() + } + #User provided timeout and exit function with an error + if($timeSpentPolling.Elapsed -gt $timeout){ + throw "Import-CrmSolutionAsync halted due to exceeding the maximum timeout of $timeout." } + #detect a failure by a failure result OR the percent being less than 100% + if(($importManifest.result.result -eq "failure") -or ($ProcPercent -lt 100) -or $anyFailuresInImport) #Must look at %age instead of this result as the result is usually wrong! + { + Write-Verbose "Import result: failed - job with ID: $importId failed at $ProcPercent complete." + throw $allErrorText + } + else + { + #at this point we appear to have imported successfully + $managedsolution = $importManifest.Managed + if($managedsolution -ne 1) + { + if($PublishChanges){ + Write-Verbose "PublishChanges set, executing: Publish-CrmAllCustomization using the same connection." + Publish-CrmAllCustomization -conn $conn + Write-Progress -Id 1 -Activity "Importing $($import.solutionname)" -CurrentOperation "Publishing Customizations" -PercentComplete 100 + Write-Verbose "Importing $($import.solutionname) - Publishing Customizations" + } + else{ + Write-Output "Import Complete, don't forget to publish customizations." + } + } + else{ + #managed + Write-Output "Import of managed solution complete." + } + } } catch { - throw $_.Exception - } + Write-Error $_.Exception + } + finally + { + Write-Progress -Id 1 -Completed -Activity "_" + Write-Progress -Id 2 -Completed -Activity "_" + } } #ImportSolutionToCrm @@ -2146,15 +2243,16 @@ function Import-CrmSolution{ } Write-Verbose "Calling .ImportSolutionToCrm() this process can take minutes..." + Write-Progress -Id 1 -Activity "Importing Solution" -CurrentOperation "Initiating import" -PercentComplete -1 $result = $conn.ImportSolutionToCrm($SolutionFilePath, [ref]$importId, $ActivatePlugIns, $OverwriteUnManagedCustomizations, $SkipDependancyOnProductUpdateCheckOnInstall,$ImportAsHoldingSolution) Write-Verbose "ImportId: $result" if ($result -eq [guid]::Empty) { throw LastCrmConnectorException($conn) } - $pollingStart = Get-Date $isProcessing = $true - $secondsSpentPolling = 0 + $timeSpentPolling = [System.Diagnostics.Stopwatch]::StartNew() + $timeout = New-TimeSpan -Seconds $MaxWaitTimeInSeconds $pollingDelaySeconds = 5 $TopPrevProcPercent = [double]0 $isProcPercentReduced = $false @@ -2162,25 +2260,28 @@ function Import-CrmSolution{ $transientFailureCount = 0; Write-Host "Import of file completed, waiting on completion of importId: $importId" try{ - while($isProcessing -and $secondsSpentPolling -lt $MaxWaitTimeInSeconds){ - #delay - Start-Sleep -Seconds $pollingDelaySeconds + Write-Progress -Id 1 -Activity "Importing Solution" -CurrentOperation "Solution file uploaded, import initiated" -PercentComplete -1 + + while($isProcessing -and $timeSpentPolling.Elapsed -lt $timeout){ + #check the import job for success/fail/inProgress try{ - $import = Get-CrmRecord -conn $conn -EntityLogicalName importjob -Id $importId -Fields solutionname,data,completedon,startedon,progress + $records = (Get-CrmRecords -conn $conn -EntityLogicalName importjob -FilterAttribute importjobid -FilterOperator eq -FilterValue $importId -Fields data,completedon,startedon,progress).CrmRecords + $import = if ($records.Count -gt 0) { $records[0] } else { $null } } catch { - if($transientFailureCount > 5){ + if($transientFailureCount -gt 5){ Write-Error "Import Job status check FAILED 5 times this could be due to a bug where the service returns a 401. Throwing lastException:"; throw $conn.LastCrmException } Write-Verbose "Import Job status check FAILED this could be due to a bug where the service returns a 401. We'll allow up to 5 failures before aborting."; $transientFailureCount++; } - #Option to use Get-CrmRecords so we can force a no-lock to prevent hangs in the retrieve - #$import = (Get-CrmRecords -conn $conn -EntityLogicalName importjob -FilterAttribute importjobid -FilterOperator eq -FilterValue $importId -Fields data,completedon,startedon,progress).CrmRecords[0] + $importManifest = ([xml]($import).data).importexportxml.solutionManifests.solutionManifest $ProcPercent = [double](Coalesce $import.progress "0") + Write-Progress -Id 1 -Activity "Importing $($import.solutionname)" -CurrentOperation "Import processing" -PercentComplete $ProcPercent + #check if processing percentage reduced at any given time if($TopPrevProcPercent -gt $ProcPercent) { @@ -2191,10 +2292,8 @@ function Import-CrmSolution{ } #Check for import completion - if($import.completedon -eq $null -and $importManifest.result.result -ne "success"){ + if($null -eq $import.completedon -and $importManifest.result.result -ne "success"){ $isProcessing = $true - $secondsSpentPolling = ([Int]((Get-Date) - $pollingStart).TotalSeconds) - Write-Host "$($secondsSPentPolling.ToString("000")) seconds of max: $MaxWaitTimeInSeconds ... ImportJob%: $ProcPercent" } else { Write-Verbose "Processing Completed at: $($import.completedon) with ImportJob%: $ProcPercent" @@ -2244,15 +2343,20 @@ function Import-CrmSolution{ $isProcessing = $false break } + + Write-Progress -Id 2 -ParentId 1 -Activity "Maximum Wait Time ($timeout)" -CurrentOperation "Remaining Time: $($timeout - $timeSpentPolling.Elapsed)" -PercentComplete ([double]($timeout - $timeSpentPolling.Elapsed).TotalSeconds * 100 / $timeout.TotalSeconds) + Start-Sleep -Seconds $pollingDelaySeconds } } Catch { Write-Error "ImportJob with ID: $importId has encountered an exception: $_ " } Finally{ $ProcPercent = ([double](Coalesce $ProcPercent 0)) + Write-Progress -Id 2 -Completed -Activity "_" + $timeSpentPolling.Stop() } #User provided timeout and exit function with an error - if($secondsSpentPolling -gt $MaxWaitTimeInSeconds){ - throw "Import-CrmSolution halted due to exceeding the maximum timeout of $MaxWaitTimeInSeconds." + if($timeSpentPolling.Elapsed -gt $timeout){ + throw "Import-CrmSolution halted due to exceeding the maximum timeout of $timeout." } #detect a failure by a failure result OR the percent being less than 100% if(($importManifest.result.result -eq "failure") -or ($ProcPercent -lt 100) -or $anyFailuresInImport) #Must look at %age instead of this result as the result is usually wrong! @@ -2269,6 +2373,7 @@ function Import-CrmSolution{ if($PublishChanges){ Write-Verbose "PublishChanges set, executing: Publish-CrmAllCustomization using the same connection." Publish-CrmAllCustomization -conn $conn + Write-Progress -Id 1 -Activity "Importing $($import.solutionname)" -CurrentOperation "Publishing Customizations" -PercentComplete 100 } else{ Write-Output "Import Complete, don't forget to publish customizations." @@ -2283,7 +2388,12 @@ function Import-CrmSolution{ catch { Write-Error $_.Exception - } + } + finally + { + Write-Progress -Id 1 -Completed -Activity "_" + Write-Progress -Id 2 -Completed -Activity "_" + } } #MergeHoldingSolution