Sonntag, 8. Dezember 2019

Testen des Word-Automation Service

Neulich kam die Frage auf, ob man den WordAutomationService von SharePoint irgendwie testen könnte.. Oder auch "direkt" starten um dabei das ULS-Log zu beobachten.

Ich habe dazu ein Skript erstellt, mit dem ein Dokument aus dem SharePoint (dieses muss existieren und in einem der unterstützten Formate vorliegen) direkt konvertiert werden kann.

<#
 .SYNOPSIS
  Tests the WordAutomationService by submitting a document and waiting for the results.

 .PARAMETER InFile
  Url (in SharePoint) that points to the document to convert
  This documents should exist.

 .PARAMETER OutFile
  Url (in SharePoint) to place the converted document
  This documents should *not* exist.

 .PARAMETER Format
  Format of the output. PDF or XPS. Default: PDF

 .EXAMPLE
  Test-WordAutomationService -InFile https://my.sp.farm/documents/word-doc.docx -OutFile https://my.sp.farm/documents/converted.pdf
  Converts https://my.sp.farm/documents/word-doc.docx to pdf and saves the result to https://my.sp.farm/documents/converted.pdf
#>

[CmdletBinding()]
[OutputType([System.Collections.Specialized.StringCollection])]
param (
  [Parameter(Mandatory=$true)]
  [string]$InFile,
  [Parameter(Mandatory=$true)]
  [string]$OutFile,
  [Parameter(Mandatory=$false)]
  [ValidateSet("pdf", "xps")]
  [string]$Format = "pdf"
)

$ErrorActionPreference="Stop"
Add-PSSnapin "Microsoft.SharePoint.PowerShell"

$tmp = [Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Word.Server")
if($tmp -eq $null) {
 throw "Could not load Microsoft.Office.Word.Server.dll. Somethong is not right..."
}
Write-Verbose "Assembly $($tmp.GetName().Name) is present and loaded"

$serviceApp = Get-SPServiceApplication | Where-Object { $_.TypeName -eq "Word Automation Services" } | Select-Object -First 1
if($null -eq $serviceApp) {
    throw "Could not find Word Automation Services Service Application. Somethong is not right..."
}
Write-Verbose "Using Service-Application $($serviceApp.Name)."
#TODO: Service app as param? what if there's more than one?!

$serviceAppProxy = Get-SPServiceApplicationProxy | Where-Object { $_.ServiceEndpointUri -eq $serviceApp.Uri.AbsoluteUri } 
Write-Verbose "Using Service-ApplicationProxy $($serviceAppProxy.Name)."

$web = $null
$url = $InFile
while ($null -eq $web) {
  if([string]::isNullOrEmpty($url)){
    throw "could not find a viable web from url: $($InFile)"
  }
  $url = (Split-Path -Path $url -Parent).Replace("\", "/")
  $web = Get-SPWeb $url -ErrorAction Ignore
}
Write-Verbose "Using web: $($web) ($($web.Url))"

$setting = New-Object "Microsoft.Office.Word.Server.Conversions.ConversionJobSettings"
$setting.OutputFormat = $Format.ToUpperInvariant()

$job = New-Object "Microsoft.Office.Word.Server.Conversions.ConversionJob" -ArgumentList @($serviceAppProxy.Id, $setting)
$job.UserToken = $web.CurrentUser.UserToken
$job.AddFile($InFile, $OutFile)
$job.Start()
Write-Verbose "Started conversion-job with id: $($job.JobId)"

$timer = Get-SPTimerJob | Where-Object { $_.TypeName -like "Word Automation Services Timer Job" } | Select-Object -First 1
Start-SPTimerJob $timer
Write-Verbose "TimerJob '$($timer.DisplayName)' started."

$status = New-Object "Microsoft.Office.Word.Server.Conversions.ConversionJobStatus" -ArgumentList @($serviceAppProxy.Id, $job.JobId, $null);
while (($status.NotStarted -ge 1) -or ($status.InProgress -ge 1)) {
    Write-Verbose "In Progress: $($status.InProgress)"
    Start-Sleep -Seconds 1
    $status.Refresh()
}

$items = $status.GetItems("Succeeded,Failed,Canceled")
Write-Verbose "Job ended. $($items.Length) items in this job."
,($items) #write them...

Das Skript erstellt einen neuen Job für den WordAutomationService, startet den entsprechenden TimerJob ("Word Automation Services Timer Job") und wartet bis die Konvertierung abgeschlossen ist. Das Ergebnis ggf. mit Fehlermeldung wird ausgegeben.

Wenn der WordAutomationService nicht läuft wie gewünscht, ist für mich immer die erste Anlaufstelle der Troubleshooting Guideline.

(Alle links beziehen sich auf SharePoint 2010 - es hat sich aber seit dem zu 2016/2019 nichts geändert.)

SQL-Alias in der ganzen SharePoint Farm

Nicht alle SharePoint-Farmen werden mit einem SQL-Alias aufgesetzt. Warum das so ist, ist mir unklar. SQL-Alias. Machen. Immer.

Ich habe letztens als "ausrede" gehört: "Das war so viel Arbeit". Okey. 5 Klicks pro Server in der Farm. Immerhin.

Das war allerdings der Anstoss dafür dass ich mir überlegte ob die Einrichtung nicht besser zu automatisieren ist. Initial hatte ich überlegt mit (Get-SPFarm).Servers zu arbeiten - das funktioniert ja aber nur, wenn die Farm schon steht - und dann könnte es für den SQL-Alias schon zu spät sein...

Dementsprechend habe ich ein Skript vorbereitet um einen SQL-Alias für eine Liste von Servern automatisiert zu setzen:

<#
  .SYNOPSIS
 Sets SQL-Aliases on multiple machines

  .PARAMETER Alias
 The Alias to use

  .PARAMETER Server
 The server to point to

  .PARAMETER Port
 The port to point to
   
  .PARAMETER Use32Bit
 set this flag to add the alias to 32bit only

  .PARAMETER Use64Bit
 set this flag to add the alias to 64bit only

  .PARAMETER Machines
 list of machines to set the alias on. Default is local

  .EXAMPLE
 Set-SqlAlias -Alias "sql1" -Server RealSqlServer 
 Sets a SQL-Alias from sql1 to point to RealSqlServer for 32 and 64Bit on the local machine only.
 
  .EXAMPLE
 Set-SqlAlias -Alias "oldServer\instance" -Server "newServer\Instance" -Use64Bit -Machines ((Get-SPFarm).Servers | ?{ $_.Role -ne "Invalid"} | Select-Object -ExpandProperty Address)
 Sets a SQL-Alias from "oldServer\instance" to point to  "newServer\Instance" for 64Bit only. This will be set on all machines of a SharePoint-Farm.
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)]
    [string]$Alias,
 [Parameter(Mandatory=$true)]
    [string]$Server,
 [Parameter()]
    [int]$Port,
 [Parameter()]
    [switch]$Use32Bit,
 [Parameter()]
    [switch]$Use64Bit,
 [Parameter()]
    [string[]]$Machines
)
$ErrorActionPreference="Stop"

if((-not $Use64Bit) -and (-not $Use32Bit)){
 # default is 32 and 64
 $Use64Bit = $true
 $Use32Bit = $true
}

if($Use64Bit -and (-not [Environment]::Is64BitProcess)){
 throw "Unable to access 64Bit-Registry from a non-64Bit-Process. Use 64Bit PowerShell."
}

if($Machines -eq $null -or $Machines.Length -eq 0){
 $Machines = @($env:COMPUTERNAME)
}

$regViews = @();
if($Use32Bit) {
 $regViews += [Microsoft.Win32.RegistryView]::Registry32
}
if($Use64Bit) {
 $regViews += [Microsoft.Win32.RegistryView]::Registry64
}

$Machines | % {
 $machine = $_
 $regViews | % {
  $view = $_
  Write-Verbose "Accessing '$($machine)' for $($view)"
  try {
   $regEdit = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $machine, $view)
  }
  catch [IO.IOException] {
   throw "Unable to connect to '$($machine)'. Error: $($_.Exception.Message)"
  }

  $key = $regEdit.OpenSubKey("SOFTWARE\Microsoft\MSSQLServer\Client\ConnectTo", $true)
  if($key -eq $null) {
   $key = $regEdit.OpenSubKey("SOFTWARE\Microsoft\MSSQLServer\Client", $true)
   if($key -eq $null) {
    throw "SQL-ClientTools not installed on $($machine)"
   }
   
   $key = $key.CreateSubKey("ConnectTo", $true)
   if($key -eq $null) {
    throw "Unable to access HKLM:SOFTWARE\Microsoft\MSSQLServer\Client\ConnectTo on '$($machine)'."
   }
  }
  
  $val = "DBMSSOCN,$($Server)"
  if($Port -gt 0) {
   $val += ",$($Port)"
  }
  $key.SetValue($Alias, $val)
  $regEdit.Close()
 }
}

Passend dazu habe ich noch ein Skript erstellt, mit dem die vorhandenen SQL-Aliase entsprechend aufgelistet werden können. (Z.B. zum Vergleichen der gesetzten Werte...)

<#
  .SYNOPSIS
 Gets SQL-Aliases on multiple machines
   
  .PARAMETER Use32Bit
 set this flag to get 32bit aliases only 

  .PARAMETER Use64Bit
 set this flag to get 64bit aliases only 

  .PARAMETER Machines
 list of machines to connect. Default is local

  .EXAMPLE
 Get-SqlAlias 
 gets all 32 and 64 Bit aliases of the local machine.
 
  .EXAMPLE
 Get-SqlAlias -Use64Bit -Machines ((Get-SPFarm).Servers | ?{ $_.Role -ne "Invalid"} | Select-Object -ExpandProperty Address)
 Gets all 64 Bit aliases for all machines of a SharePoint-Farm.
#>
[CmdletBinding()]
param(
 [Parameter()]
    [switch]$Use32Bit,
 [Parameter()]
    [switch]$Use64Bit,
 [Parameter()]
    [string[]]$Machines
)
$ErrorActionPreference="Stop"

if((-not $Use64Bit) -and (-not $Use32Bit)){
 # default is 32 and 64
 $Use64Bit = $true
 $Use32Bit = $true
}

if($Use64Bit -and (-not [Environment]::Is64BitProcess)){
 throw "Unable to access 64Bit-Registry from a non-64Bit-Process. Use 64Bit PowerShell."
}

if($Machines -eq $null -or $Machines.Length -eq 0){
 $Machines = @($env:COMPUTERNAME)
}

$regViews = @();
if($Use32Bit) {
 $regViews += [Microsoft.Win32.RegistryView]::Registry32
}
if($Use64Bit) {
 $regViews += [Microsoft.Win32.RegistryView]::Registry64
}

$Machines | % {
 $machine = $_
 $regViews | % {
  $view = $_
  Write-Verbose "Accessing '$($machine)' for $($view)"
  try {
   $regEdit = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $machine, $view)
  }
  catch [IO.IOException] {
   throw "Unable to connect to '$($machine)'. Error: $($_.Exception.Message)"
  }
  
  $key = $regEdit.OpenSubKey("SOFTWARE\Microsoft\MSSQLServer\Client\ConnectTo", $true)
  if($key -eq $null) {
   $regEdit.Close()
   return
  }
  
  $names = $key.GetValueNames()
  $names | % {
   $parts = $key.GetValue($_).Split(",")
   $server = $parts[1]
   $port = ""
   if($parts.Length -gt 2) {
    $port = $parts[2]
   }
   [pscustomobject]@{
    Machine = $machine;
    RegistryView = $view;
    Alias = $_;
    Server = $server;
    Port = $port
   }
  }
  
  $regEdit.Close()
 }
}