Sandbox solutions are a great feature of the SharePoint 2010 platform. But as these solutions are deployed in various locations across your farm or site collection, the question of how to manage them arises.

We're providing a PowerShell script to help you with this. It's pretty simple but can be extended to do more in order to fit into the tools you're already using to manage your farms and site collections.

Let me describe what this script does.

Every Sandbox Solution that is deployed has a Solution ID associated with it. Inside that solution, you will find one or more features with a Feature ID and a version number. For a given Solution Id, this script will walk your entire farm or site collection and generate a log telling you all the locations where that solution is found. It will then look at each feature within the Solution and add a log entry indicating which version of that feature is deployed. It can also perform the upgrade if a new version of the Solution is provided. This is a flag so the script can perform the upgrade or simply be used as a means for gathering information on the solution.

The script takes the following parameters:

Permissions

The log format is as follows:

A few Things to Note

To upgrade a solution, the SharePoint 2010 platform expects the solution filenames to be different. If they are the same, you will get an error telling you that the solution is already active. When you upgrade a solution, the solution id will be used to find existing versions of that solution and THEN look at the version details. The upgrade will only move you to a new version. It will not roll back to an old version. And if the versions are the same, it will do nothing.

We hope this script helps you manage the solutions you are deploying and makes upgrades much easier.

# SharePoint DLL

[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")

#Cmdlet Install

Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue

#Log file definition

$intro = "User Solution Upgrader v1.0 - Log File"

$date = (Get-Date).ToString('yyyyMMdd')

Set-Variable -Name ForReading -value 1 -Option Constant

Set-Variable -Name ForWriting -value 2 -Option Constant

Set-Variable -Name ForAppending -value 8 -Option Constant

#Assume that the current "Domain\User" is the box admin entitled to perform the reconaissance and upgrade actions.

$admin = $env:UserDomain + "\" + $env:UserName

if ($env:UserDomain -eq $null)

{

$admin = $admin.substring(1)

}

# Get the script's parent folder. This is where the log file will be written.

$logFolder = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent

$getACL = Get-Acl $logFolder

$access = $getACL.Access | Where { $_.IdentityReference -eq $admin }

if ($access -eq $null -or

($access.FileSystemRights -ne [System.Security.AccessControl.FileSystemRights]::FullControl -and

($access.FileSystemRights -ne [System.Security.AccessControl.FileSystemRights]::Write -and

$access.FileSystemRights -ne [System.Security.AccessControl.FileSystemRights]::CreateFiles)))

{

$logFolder = $env:userprofile

}

# Add the log file name to the path.

$logFileFullPath = Join-Path $logFolder "usersolutionupgrader-$date.txt"

# Open file

$fo = New-Object -com Scripting.FileSystemObject

$f = $fo.OpenTextFile($logFileFullPath, $ForAppending, $true)

if ($f -eq $null)

{

$logFolder = $env:userprofile

$logFileFullPath = Join-Path $logFolder "usersolutionupgrader-$date.txt"

$f = $fo.OpenTextFile($logFileFullPath, $ForAppending, $true)

}

#Confirm log file on screen

Write-Host ("Log Path: " + $logFileFullPath)

#Write intro line

$f.WriteLine()

$f.WriteLine($intro)

function WriteLog

{

$dt = Get-Date

$logMsg = $dt.ToShortDateString() + " " + $dt.ToShortTimeString() + "`t" + $args[0]

$f.WriteLine($logMsg)

Write-Output($logMsg) #Turn this on when you need console output

}

function exitScript()

{

if ($f -ne $null)

{

$f.Close()

}

Write-Host "Exiting script due to error."

exit

}

WriteLog ("Current user (admin): $admin")

#solution details

$solFile = ""

$targetSolutionId = ""

$upgrade = $false

$myError = $null

$haveCurrentFeatures = $false

$haveNewFeatures = $false

$numSites = 0

$oldFeatures = @{}

$newFeatures = @{}

$solSites = New-Object System.Collections.ArrayList

$solFailSites = New-Object System.Collections.ArrayList

#Parse the commandline parameters

if (($args -eq $null) -or ($args.Count -lt 2))

{

WriteLog("Error: This script has been called without enough parameters. Listing your parameters below:")

WriteLog($args)

if ($f -ne $null)

{

$f.Close()

}

exitScript

}

else

{

$badParameters = $false

foreach ($arg in $args)

{

switch ($arg)

{

"-upgrade" {$upgrade = $true}

"-path" {[void] $foreach.MoveNext(); $solFile = $foreach.Current}

default {$badParameters = $true}

}

}

if ($badParameters -eq $true)

{

WriteLog("You passed in a bad parameter.")

exitScript

}

#Validate parameters

$extension = [IO.Path]::GetExtension($solFile)

if (($extension -eq $null) -or ($extension -ne ".wsp") -or (!(Test-Path $solFile)))

{

WriteLog("Error: The solution file name is not of WSP format or is an invalid file.")

exitScript

}

#Confirm parameter values captured

$fileName = Split-Path $solFile -Leaf

WriteLog("Solution Path is: `t" + $solFile)

WriteLog("Solution Name is: `t" + $fileName)

if ($upgrade)

{

WriteLog "The tool will attempt to perform UPGRADE on the site collections"

}

WriteLog ""

}

#Get solution ID from given WSP

$tempPath = Join-Path $env:temp "upgrader" | Join-Path -ChildPath $fileName

$shell = New-Object -ComObject "Shell.Application" -ErrorAction:SilentlyContinue -ErrorVariable myError

if ($myError -ne $null)

{

WriteLog("FAILED to create the Shell.Application object.")

WriteLog("Error: " + $myError)

$myError = $null

}

[IO.Directory]::CreateDirectory($tempPath) | Out-Null

$tempFolder = $shell.NameSpace($tempPath)

$tempFolder.CopyHere($solFile)

#Take the file name ("yourfile.wsp") from the original path

$tempSolPath = Join-Path $tempPath $fileName

if (!(Test-Path $tempSolPath))

{

WriteLog "Error: Failed to copy WSP file to temp location."

exitScript

}

#Rename the WSP file to have CAB extension in order to facilitate CAB extraction

$cabFileName = [System.IO.Path]::GetFileNameWithoutExtension($fileName) + ".cab"

Rename-Item $tempSolPath $cabFileName

$cabPath = Join-Path $tempPath $cabFileName

$sourceWsp = $shell.NameSpace($cabPath).items()

$tempFolder.CopyHere($sourceWsp)

$manifestPath = Join-Path $tempPath "manifest.xml"

[xml]$manifest = Get-Content $manifestPath

$targetSolutionId = $manifest.Solution.SolutionId

#Validate Solution GUID

[Guid]$testGuid = "B80D56EC-5899-459d-83B4-1AE0BB8418E4"

if (($targetSolutionId -eq $null) -or ($targetSolutionId.Length -lt 36) -or

([System.ComponentModel.TypeDescriptor]::GetConverter($testGuid).ConvertFromString($targetSolutionId) -eq $null))

{

WriteLog("Error: Target solution ID is invalid: " + $stringSolutionId)

exitScript

}

WriteLog("Extracted solution ID: $targetSolutionId from manifest.xml")

WriteLog("")

#Now delete temp folder.

Remove-Item $tempPath\*

Remove-Item $tempPath

#Go through Content DBs

WriteLog ("Looking for Solution Id: " + $targetSolutionId + " in all Content Databases`n")

$dbs = Get-SPContentDatabase

foreach ($contentdb in $dbs)

{

#Web App Level

$webAppUrl = $contentdb.WebApplication.Url

#Get WebApp

$webApp = Get-SPWebApplication -Identity $webAppUrl

$policy = $webApp.Policies[$admin]

$policyAdded = $false

$roleAdded = $false

#If the admin doesn't have Full Control, it will be granted as follows:

if ($policy -eq $null)

{

$policy = $webApp.Policies.Add($admin, "")

$webAppModified = $true

$policyAdded = $true

WriteLog "Added a policy entry for user '$admin'."

}

$fullRole = $webApp.PolicyRoles.GetSpecialRole([Microsoft.SharePoint.Administration.SPPolicyRoleType]::FullControl)

if ($policy.PolicyRoleBindings[$fullRole] -eq $null)

{

$policy.PolicyRoleBindings.Add($fullRole)

$webAppModified = $true

$roleAdded = $true

WriteLog "Full Control added for '$admin' to Web Application '$webAppUrl'"

}

if ($webAppModified)

{

$webApp.Update()

$webAppModified = $false

}

#Done. Have Full Control

WriteLog ("Entering Web Application '$webAppUrl'.`n")

Get-SPSite -WebApplication $webApp -Limit ALL | % {

$site = $_

$solution = $null

$foundSolution = $false

#Scan for solution here

Get-SPUserSolution -Site $_ | Where { $_.Status -eq [Microsoft.Sharepoint.SPUserSolutionStatus]::Activated -and $_.SolutionId -eq $targetSolutionId } | % {

if ($foundSolution -eq $false)

{

$foundSolution = $true

$solution = $_

if ($haveCurrentFeatures -eq $false)

{

Get-SPFeature -Sandboxed -Site $site | Where { $_.SolutionId -eq $targetSolutionId } | % {$oldFeatures.Add($_.Id, $_)}

$haveCurrentFeatures = $true

}

if ($upgrade -eq $false)

{

$solSites.Add($site.Url) | Out-Null

}

$solutionHash = $_.Signature

WriteLog ("Found site collection: " + $site.Url)

}

}

$numSites ++

#DoUpgrade here

if ($upgrade -and $foundSolution)

{

$successAdd = $false

WriteLog ("Uploading new solution file as: $fileName")

#Add + Upgrade solution

$myError = $null

Add-SPUserSolution -LiteralPath $solFile -Site $site -ErrorAction:SilentlyContinue -ErrorVariable myError -Confirm:$false

if ($myError -ne $null)

{

WriteLog("Site collection '" + $site.Url + "' FAILED to upload the new solution.")

WriteLog("Error: " + $myError)

$myError = $null

}

else

{

$successAdd = $true

}

$addedSolution = Get-SPUserSolution -Identity $fileName -Site $site

if ($addedSolution -ne $null)

{

WriteLog ("Found solution $fileName in the Solutions Gallery. Attempting to use it...")

#First check for already updated solution

if ($addedSolution.Signature -eq $solutionHash)

{

#This means we have the same version installed. Just skip it. And delete our copy!

WriteLog ("New solution is already active on this site collection.")

if ($successAdd -eq $true)

{

#Remove the new solution (the dupe)

WriteLog ("Removing file: $fileName")

Remove-SPUserSolution -Identity $addedSolution -Site $site -Confirm:$false

if (!($solFailSites.Contains($site.Url)))

{

$solFailSites.Add($site.Url) | Out-Null

}

}

}

else

{

#Perform upgrade

Update-SPUserSolution -Identity $solution -Site $site -ToSolution $addedSolution -ErrorAction:SilentlyContinue -ErrorVariable myError -Confirm:$false

if ($myError -ne $null)

{

WriteLog("Site collection '" + $site.Url + "' FAILED to upgrade to the new solution.")

WriteLog("Error: " + $myError)

if (!($solFailSites.Contains($site.Url)))

{

$solFailSites.Add($site.Url) | Out-Null

}

$myError = $null

}

if (!($solFailSites.Contains($site.Url)))

{

#Upgrade succeeded

WriteLog("Site collection '" + $site.Url + "' has been upgraded to the new solution.")

WriteLog ""

$solSites.Add($site.Url) | Out-Null

#Record results AFTER upgrade

if ($haveNewFeatures -eq $false)

{

Get-SPFeature -Sandboxed -Site $site | Where { $_.SolutionId -eq $targetSolutionId } | %{$newFeatures.Add($_.Id, $_)}

$haveNewFeatures = $true

}

}

}

}

else

{

if (!($solFailSites.Contains($site.Url)))

{

$solFailSites.Add($site.Url) | Out-Null

}

}

}

$site.Close();

}

#Close permissions and webApp

if ($roleAdded)

{

$policy.PolicyRoleBindings.RemoveById($fullRole.Id)

$webAppModified = $true

WriteLog "Removed Full Control for '$admin' from Web Application '$webAppUrl'"

}

if ($policyAdded)

{

$webApp.Policies.Remove($admin)

$webAppModified = $true

WriteLog "Removed the policy entry for user '$admin'."

}

if ($webAppModified)

{

$webApp.Update()

$webAppModified = $false

}

WriteLog ""

WriteLog "Done with Web Application."

WriteLog ""

}

#Final tally of site collections

WriteLog("Analysis of site collection upgrades for solution ID $targetSolutionId ...")

WriteLog("We have processed a total of $numSites site collections.")

WriteLog("")

if ($upgrade)

{

#Site Collection Summary

if ($solSites.Count -eq 0)

{

WriteLog("No site collections were upgraded. Refer to this log for any upgrade errors.")

}

else

{

WriteLog("Listing site collections that have been upgraded:")

foreach ($siteUrl in $solSites)

{

WriteLog($siteUrl)

}

}

if ($solFailSites.Count -ne 0)

{

WriteLog("Listing site collections that FAILED to upgrade:")

foreach ($siteUrl in $solFailSites)

{

WriteLog($siteUrl)

}

}

#Feature Upgrade Summary

if ($newFeatures.Count -gt 0)

{

WriteLog ""

WriteLog "Feature upgrade summary for the New User Solution:"

foreach($fKey in $newFeatures.Keys)

{

$fDef = $oldFeatures[$fKey]

$fDef2 = $newFeatures[$fKey]

#Feature ID and DisplayName

WriteLog("Feature ID: " + $fDef2.Id + "`t DisplayName: " + $fDef2.DisplayName)

#Feature Version

if ($fDef -eq $null)

{

#New feature added.

WriteLog("This feature has been newly added.`t`t`t Version " + $fDef2.Version)

}

else

{

if ($fDef.Version -eq $fDef2.Version)

{

WriteLog("This feature has been unchanged.`t`t`t Version " + $fDef2.Version)

}

else

{

WriteLog("Feature went from Version " + $fDef.Version + " to Version " + $fDef2.Version)

}

}

WriteLog ""

}

WriteLog ("The new solution holds a total of " + $newFeatures.Count + " feature(s).")

}

}

else

{

if ($solSites.Count -eq 0)

{

WriteLog("No site collections have been found.")

}

else

{

WriteLog("We have found " + $solSites.Count + " site collections, as follows (no upgrade action performed):")

foreach ($siteUrl in $solSites)

{

WriteLog($siteUrl)

}

}

WriteLog ""

WriteLog "Feature summary for current User Solution:"

foreach($fKey in $oldFeatures.Keys)

{

$fDef = $oldFeatures[$fKey]

WriteLog ("Feature DisplayName: `t" + $fDef.DisplayName)

WriteLog ("Feature Version: `t" + $fDef.Version)

WriteLog ("Feature Id: `t`t" + $fDef.Id)

}

WriteLog ""

WriteLog ("Found a total of " + $oldFeatures.Count + " feature(s).")

WriteLog ""

}

WriteLog ""

#Close the file handle.

if ($f -ne $null)

{

$f.Close()

}