Running a powershell script using multiple threads - multithreading

I'm trying to execute a powershell script from within another powershell script. My current script runs fine, but I'd like to try and speed it up if possible.
What my script does is import a list of contacts into Each users Contacts folder through EWS.
In my powershell script the script that handles the importing I call it like this (ImportContacts is a Function without any arguments):
. $rootPath\ImportContacts\ImportContacts.ps1
ImportContacts
When I run it normally, as I mentioned above everything works fine, I just would like to speed it up. I tried following some examples of implementing runspacepool in Powershell to take advantage of using multiple threads, but it doesn't seem to be working properly for me. I'm sure it's a silly syntax error, but I have this currently:
Measure-Command{
$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$RunspacePool.Open()
$Jobs = #()
ImportContacts | Foreach-Object {
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$PowerShell.AddScript({ImportContacts})
$Jobs += $PowerShell.BeginInvoke()
}
while ($Jobs.IsCompleted -contains $false)
{
Start-Sleep 1
}}
This seems to do the job, but I can't tell a difference with the speed.
EDIT 5-15-21
To better assist with the question, here is how I retrieve the user data. I have a file I call called "ExportedContacts.ps1"
It's called like so:
$Users = & $rootPath\ExportContacts\ExportContacts.ps1
The contents of the file is this
$Users = Get-ADUser -Filter * -Properties extensionAttribute2, middlename, mobile, OfficePhone, GivenName, Surname, DisplayName, EmailAddress, Title, Company, Department, thumbnailPhoto | Where-Object {($_.extensionAttribute2 -like "DynamicDistro") -AND (($_.Mobile -ne $NULL) -OR ($_.OfficePhone -ne $NULL))} | Select-Object #{Name="First Name";Expression={$_.GivenName}},#{Name="Last Name";Expression={$_.Surname}},#{Name="Display Name";Expression={$_.DisplayName}},#{Name="Job Title";Expression={$_.Title}},#{Name="Company";Expression={$_.Company}},#{Name="Department";Expression={$_.Department}},#{Name="Mobile Phone";Expression={$_.Mobile}},#{Name="Home Phone";Expression={$_.OfficePhone}}, #{Name="Middle Name";Expression={$_.MiddleName}}, #{Name="E-mail Address";Expression={$_.EmailAddress}}, thumbnailPhoto | Sort-Object "Last Name"
return $Users
Then I import those contacts, similarly to how I mentioned above. The content of the import is as follows:
Function ImportContacts
{
Write-Host "Importing Contacts. This can take several minutes."
#******************************************************************
foreach ($ContactItem in $Users)
{
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress);
$ExchangeContact = New-Object Microsoft.Exchange.WebServices.Data.Contact($service);
$ExchangeContact.NickName = ('{0} {1}' -f $ContactItem."First Name", $ContactItem."Last Name"). Trim()
$ExchangeContact.DisplayName = $ExchangeContact.NickName;
$ExchangeContact.FileAs = $ExchangeContact.NickName;
$ExchangeContact.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1] = $ContactItem."E-mail Address";
$ExchangeContact.PhoneNumbers[[Microsoft.Exchange.WebServices.Data.PhoneNumberKey]::HomePhone] = $ContactItem."Home Phone";
$ExchangeContact.PhoneNumbers[[Microsoft.Exchange.WebServices.Data.PhoneNumberKey]::MobilePhone] = $ContactItem."Mobile Phone";
$ExchangeContact.Department = $ContactItem."Department";
$ExchangeContact.CompanyName = $ContactItem."Company";
$ExchangeContact.JobTitle = $ContactItem."Job Title";
$ExchangeContact.MiddleName = $ContactItem."Middle Name";
# Save the contact
$ExchangeContact.Save($ContactsFolder.Id);
}
}
I am also including the File that creates the contacts folder I specify and also deletes existing contacts (if the folder already exists), so that each import is a clean and updated import. I'd be curious if there is a faster way to clean the existing items?
Function CreateContactsFolder
{
Write-Host "Creating Contacts Folder and Cleaning up stale items. This can take a couple minutes."
Try
{
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress);
$RootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot)
$RootFolder.Load()
#Check to see if they have a contacts folder that we want
$FolderView = new-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)
$ContactsFolderSearch = $RootFolder.FindFolders($FolderView) | Where-Object {$_.DisplayName -eq $FolderName}
if($ContactsFolderSearch)
{
$ContactsFolder = [Microsoft.Exchange.WebServices.Data.ContactsFolder]::Bind($service,$ContactsFolderSearch.Id);
#If folder exists, connect to it. Clear existing Contacts, and reupload new (UPDATED) Contact Info
Write-Host "Folder alreads exists. We will remove all contacts under this folder."
# Attempt to empty the target folder up to 10 times.
$tries = 0
$max_tries = 0
while ($tries -lt 2)
{
try
{
$tries++
$ErrorActionPreference='Stop'
$ContactsFolder.Empty([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete, $true)
$tries++
}
catch
{
$ErrorActionPreference='SilentlyContinue'
$rnd = Get-Random -Minimum 1 -Maximum 10
Start-Sleep -Seconds $rnd
$tries = $tries - 1
$max_tries++
if ($max_tries -gt 100)
{
Write-Host "Error; Cannot empty the target folder; `t$EmailAddress"
}
}
}
}
else
{
#Contact Folder doesn't exist. Let's create it
try
{
Write-Host "Creating new Contacts Folder called $FolderName"
$ContactsFolder = New-Object Microsoft.Exchange.WebServices.Data.ContactsFolder($service);
$ContactsFolder.DisplayName = $FolderName
$ContactsFolder.Save([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot)
}
catch
{
Write-Host "Error; Cannot create the target folder; `t$EmailAddress"
}
}
return $ContactsFolder
}
Catch
{
Write-Host "Couldn't connect to the user's mailbox. Make sure the admin account you're using to connect to has App Impersonization permissions"
Write-Host "Check this link for more info: https://help.bittitan.com/hc/en-us/articles/115008098447-The-account-does-not-have-permission-to-impersonate-the-requested-user"
}
}

Related

Error: Cannot find an overload for "restore" and the argument count: "1"

I am getting this error from the following code. It's coming from $Context.Load($RecycleBinItems). Any idea what's wrong with the code? I am attempting to restore all recyclebin items.
Add-Type -Path "C:\Program Files\WindowsPowerShell\Modules\SharePointPnPPowerShellOnline\3.17.2001.2\Microsoft.SharePoint.Client.dll"
Add-Type -Path "C:\Program Files\WindowsPowerShell\Modules\SharePointPnPPowerShellOnline\3.17.2001.2\Microsoft.SharePoint.Client.Runtime.dll"
Import-Module 'Microsoft.PowerShell.Security'
#Get the Site Owners Credentials to connect the SharePoint
$SiteUrl = "https://phaselinknet.sharepoint.com"
$UserName = Read-host "Enter the Email ID"
$Password = Read-host - assecurestring "Enter Password for $AdminUserName"
$Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $Password)
# Once Connected, get the Site information using current Context objects
Try {
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl)
$Context.Credentials = $Credentials
$Site = $Context.Site
$RecycleBinItems = $Site.RecycleBin
$Context.Load($Site)
$Context.Load($RecycleBinItems)
$Context.ExecuteQuery()
Write-Host "Total Number of Files found in Recycle Bin:" $RecycleBinItems.Count
}
catch {
write - host "Error: $($_.Exception.Message)" - foregroundcolor Red
}
# using for loop to restore the item one by one
Try {
if($RecycleBinItems)
{
foreach($Item in $RecycleBinItems)
{
$Site.RecycleBin.restore($Item.ID)
#Write-Host "Item restored:"$Item.Title
}
}
}
catch {
write-host "Error: $($_.Exception.Message)" -foregroundcolor Red
}
The error message is giving you you answer. There is not a version of the method Restore that takes 1 parameter.
You need to load up a list of items simular to this
$Item = $RecycleBin | Where{$_.Title -eq $ItemName}
Then call restore for the items.
if($Item -ne $null)
{
$Item.Restore()
}
Thanks for the tip. So I load up the first 10 items in the recyclebin, and Write-Host does write out the correct files, but the $Item.Restore() does noting as the files are still not restored:
$itemsToRestore = #()
for ($i = 0; $i -lt 10; $i++)
{
$Item = $RecycleBinItems[$i]
$itemsToRestore += $Item
}
Write-Host "Total Number of Files to Restore:" $itemsToRestore.Count
foreach($item in $itemsToRestore)
{
Write-Host "Item:" $Item.Title
$item.Restore()
}
I found the problem. I missed $Context.ExecuteQuery() after $Item.Restore(). It works now.

Change multiple files content type programatically using CSOM for SharePoint Online

I am attempting to change all the files in a library from one content type to another. This is in SharePoint Online so I'm using the CSOM. I am new to this so I'm stuck at where to go from what I have below.
I think my biggest issue is getting the values of the content types. That and I'm so used to SP On Premise I'm having trouble grasping this CSOM stuff. Much thanks to anyone that can help point me in the right direction!
#Load necessary module to connect to SPOService
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") | Out-Null
#Login Information for script
$User = "user"
$Pass = "password"
$WebUrl = "SiteURL"
#Connect to SharePoint Online service
Write-Host "Logging into SharePoint online service." -ForegroundColor Green
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($WebUrl)
$Context.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($User, (ConvertTo-SecureString $Pass -AsPlainText -Force))
#Get the Necessary List
Write-Host "Getting the required list." -ForegroundColor Green
$List = $Context.Web.Lists.GetByTitle("TestLibrary")
Write-Host "Getting the Content Types." -ForegroundColor Green
$oldCT = $list.ContentTypes("OldCTName")
$newCT = $list.ContentTypes("NewCTName")
$newCTID = $newCT.ID
$Query = [Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery(1000);
$Items = $List.GetItems($Query);
$Context.Load($Items);
$Context.ExecuteQuery();
#Check if the values specified for the content types actually exist on the list
if (($oldCT -ne $null) -and ($newCT -ne $null))
{
#Go through each item in the list
#Edit existing list items
foreach($item in $Items)
{
#Check if the item content type currently equals the old content type specified
if ($_.ContentType.Name -eq $oldCT.Name)
{
#Check the check out status of the file
if ($_.File.CheckOutType -eq "None")
{
#Change the content type association for the item
$_.File.CheckOut()
write-host "Resetting content type for file" $_.Name "from" $oldCT.Name "to" $newCT.Name
$_["ContentTypeId"] = $newCTID
$_.Update()
$_.File.CheckIn("Content type changed to " + $newCT.Name, 1)
}
else
{
write-host "File" $_.Name "is checked out to" $_.File.CheckedOutByUser.ToString() "and cannot be modified"
}
}
else
{
write-host "File" $_.Name "is associated with the content type" $_.ContentType.Name "and shall not be modified"
}
}
}
else
{
write-host "One of the content types specified has not been attached to the list"$list.Title
}
$Context.ExecuteQuery();
}

Utilize Results from Synchronized Hashtable (Runspacepool 6000+ clients)

Adapting a script to do multiple functions, starting with test-connection to gather data, will be hitting 6000+ machines so I am using RunspacePools adapted from the below site;
http://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/
The data comes out as below, I would like to get it sorted into an array (I think that's the terminology), so I can sort the data via results. This will be adapted to multiple other functions pulling anything from Serial Numbers to IAVM data.
Is there any way I can use the comma delimited data and have it spit the Values below into columns? IE
Name IPAddress ResponseTime Subnet
x qwe qweeqwe qweqwe
The added values aren't so important at the moment, just the ability to add the values and pull them.
Name Value
—- —–
x-410ZWG \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410ZWG",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-47045Q \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-47045Q",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-440J26 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-440J26",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-410Y45 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410Y45",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DJKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DJKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
nonexistant
x-DDMVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DDMVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-470481 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-470481",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DHKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DHKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-430XXF \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-430XXF",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DLKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DLKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-410S86 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410S86",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-SCH004 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-SCH004",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-431KMS
x-440J22 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-440J22",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
Thank for any help!
Code currently
Function Get-RunspaceData {
[cmdletbinding()]
param(
[switch]$Wait
)
Do {
$more = $false
Foreach($runspace in $runspaces) {
If ($runspace.Runspace.isCompleted) {
$runspace.powershell.EndInvoke($runspace.Runspace)
$runspace.powershell.dispose()
$runspace.Runspace = $null
$runspace.powershell = $null
} ElseIf ($runspace.Runspace -ne $null) {
$more = $true
}
}
If ($more -AND $PSBoundParameters['Wait']) {
Start-Sleep -Milliseconds 100
}
#Clean out unused runspace jobs
$temphash = $runspaces.clone()
$temphash | Where {
$_.runspace -eq $Null
} | ForEach {
Write-Verbose ("Removing {0}" -f $_.computer)
$Runspaces.remove($_)
}
Write-Host ("Remaining Runspace Jobs: {0}" -f ((#($runspaces | Where {$_.Runspace -ne $Null}).Count)))
} while ($more -AND $PSBoundParameters['Wait'])
}
#Begin
#What each runspace will do
$ScriptBlock = {
Param ($computer,$hash)
$Ping = test-connection $computer -count 1 -ea 0
$hash[$Computer]= $Ping
}
#Setup the runspace
$Script:runspaces = New-Object System.Collections.ArrayList
# Data table for all of the runspaces
$hash = [hashtable]::Synchronized(#{})
$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$runspacepool = [runspacefactory]::CreateRunspacePool(1, 100, $sessionstate, $Host)
$runspacepool.Open()
#Process
ForEach ($Computer in $Computername) {
#Create the powershell instance and supply the scriptblock with the other parameters
$powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($computer).AddArgument($hash)
#Add the runspace into the powershell instance
$powershell.RunspacePool = $runspacepool
#Create a temporary collection for each runspace
$temp = "" | Select-Object PowerShell,Runspace,Computer
$Temp.Computer = $Computer
$temp.PowerShell = $powershell
#Save the handle output when calling BeginInvoke() that will be used later to end the runspace
$temp.Runspace = $powershell.BeginInvoke()
Write-Verbose ("Adding {0} collection" -f $temp.Computer)
$runspaces.Add($temp) | Out-Null
}
# Wait for all runspaces to finish
#End
Get-RunspaceData -Wait
$stoptimer = Get-Date
#Display info, and display in GridView
Write-Host
Write-Host "Availability check complete!" -ForegroundColor Cyan
"Execution Time: {0} Minutes" -f [math]::round(($stoptimer – $starttimer).TotalMinutes , 2)
$hash | ogv
When you use runspaces, you write the scriptblock for the runspace pretty much the same way you would for a function. You write whatever you want the return to be to the pipeline, and then either assign it to a variable, pipe it to another cmdlet or function, or just let it output to the console. The difference is that while the function returns it's results automatically, with the runspace they collect in the runspace output buffer and aren't returned until you do the .EndInvoke() on the runspace handle.
As a general rule, the objective of a Powershell script is (or should be) to create objects, and the objective of using the runspaces is to speed up the process by multi-threading. You could return string data from the runspaces back to the main script and then use that to create objects there, but that's going to be a single threaded process. Do your object creation in the runspace, so that it's also multi-threaded.
Here's a sample script that uses a runspace pool to do a pingsweep of a class C subnet:
Param (
[int]$timeout = 200
)
$scriptPath = (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent)
While (
($network -notmatch "\d{1,3}\.\d{1,3}\.\d{1,3}\.0") -and -not
($network -as [ipaddress])
)
{ $network = read-host 'Enter network to scan (ex. 10.106.31.0)' }
$scriptblock =
{
Param (
[string]$network,
[int]$LastOctet,
[int]$timeout
)
$options = new-object system.net.networkinformation.pingoptions
$options.TTL = 128
$options.DontFragment = $false
$buffer=([system.text.encoding]::ASCII).getbytes('a'*32)
$Address = $($network.trim("0")) + $LastOctet
$ping = new-object system.net.networkinformation.ping
$reply = $ping.Send($Address,$timeout,$buffer,$options)
Try { $hostname = ([System.Net.Dns]::GetHostEntry($Address)).hostname }
Catch { $hostname = 'No RDNS' }
if ( $reply.status -eq 'Success' )
{ $ping_result = 'Yes' }
else { $ping_result = 'No' }
[PSCustomObject]#{
Address = $Address
Ping = $ping_result
DNS = $hostname
}
}
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(100,100)
$RunspacePool.Open()
$Jobs =
foreach ( $LastOctet in 1..254 )
{
$Job = [powershell]::Create().
AddScript($ScriptBlock).
AddArgument($Network).
AddArgument($LastOctet).
AddArgument($Timeout)
$Job.RunspacePool = $RunspacePool
[PSCustomObject]#{
Pipe = $Job
Result = $Job.BeginInvoke()
}
}
Write-Host 'Working..' -NoNewline
Do {
Write-Host '.' -NoNewline
Start-Sleep -Seconds 1
} While ( $Jobs.Result.IsCompleted -contains $false)
Write-Host ' Done! Writing output file.'
Write-host "Output file is $scriptPath\$network.Ping.csv"
$(ForEach ($Job in $Jobs)
{ $Job.Pipe.EndInvoke($Job.Result) }) |
Export-Csv $scriptPath\$network.ping.csv -NoTypeInformation
$RunspacePool.Close()
$RunspacePool.Dispose()
The runspace script does a ping on each address, and if it gets successful ping attempts to resolve the host name from DNS. Then it builds a custom object from that data, which is output to the pipeline. At the end, those objects are returned when the .EndInvoke() is done on the runspace jobs and piped directly into Export-CSV, but it could just as easily be output to the console, or saved into a variable.

Get-ADUser can't find all users

I'm reading in a list of users from a Excel spreadsheet one row at a time. Once I get that I'm attempting to get the user object in Active Directory that matches the username I pulled from the Excel file. Unfortunately, it gets the first user but then every user after that it says that it can't find them. Here's what I'm doing:
do
{
# Get the user's login name
$userPrincipalName = $objWorksheet.Cells.Item($intRow, 1).Value()
# Get the user description
$description = $objWorksheet.Cells.Item($intRow, 2).Value()
$intRow++
$user = Get-ADUser -Filter "userPrincipalName -eq '$userPrincipalName'" -Properties Description
if ($user)
{
if (!($user.Description))
{
$user | Set-ADUser -Description $description
Write-Host "User" $userPrincipalName "was altered."
$num_of_users_altered++
}
else
{
Write-Host "User" $userPrincipalName "already has a description."
}
}
else
{
Write-Host "User" $userPrincipalName "was not found."
$num_of_users_not_altered++
}
}
while ($objWorksheet.Cells.Item($intRow, 1).Value() -ne $null)
Now the first user (the one that is found) is in a different OU from the others. I've tried removing that user from the spreadsheet to see if the issue with them being in different OU's but it just didn't find any of them. Any ideas what I might be doing wrong?
Your loop looks like it should work.
Are you sure that you have UPNs (user1#mydomain.com) for all users, and not samAccountName (user1)?
Do you get any results if you manually run Get-ADUser -Filter "userPrincipalName -eq '$userPrincipalName'" -Properties Description (replace $userPrincipalName with one of the values you get in a "not found"-message?
What happends if you use this ($userPrincipalName = $objWorksheet.Cells.Item($intRow, 1).Value().Trim())?
Does it make a difference if convert your file to CSV? Personally, I would always recommend using a CSV-file with PowerShell. CSV-files are alot easier to work with.
sample:
Import-CSV -Path "c:\mycsvfile.csv" | ForEach-Object {
#Modify to match your column names
$upn = $_.userPrincipalName.Trim()
$desc = $_.description.Trim()
$user = Get-ADUser -Filter "userPrincipalName -eq '$upn'" -Properties Description
if ($user)
{
if (!($user.Description))
{
$user | Set-ADUser -Description $desc
Write-Host "User" $upn "was altered."
$num_of_users_altered++
}
else
{
Write-Host "User" $upn "already has a description."
}
}
else
{
Write-Host "User" $upn "was not found."
$num_of_users_not_altered++
}
}

Powershell to list all pages with their layout across the rootweb and all subwebs?

I need to be able to create a report of all existing pages and their page layout.
I have the following powershell script but even using Recursive its only returning me the ones from the root web.
filter Get-PublishingPages {
$pubweb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($_)
$query = new-object Microsoft.SharePoint.SPQuery
$query.ViewAttributes = "Scope='Recursive'"
$pubweb.GetPublishingPages($query)
}
$url="https://xxxxl.com"
get-spweb $url | Get-PublishingPages | select Uri, Title, #{Name='PageLayout';Expression={$_.Layout.ServerRelativeUrl}}
This worked for me.
filter Get-PublishingPages {
$pubweb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($_)
$query = new-object Microsoft.SharePoint.SPQuery
$query.ViewAttributes = "Scope='Recursive'"
$pubweb.GetPublishingPages($query)
}
$str = "http://yourdomain.com" // your URL
if($str -eq $null )
{
Write-Host “Enter a valid URL”
return
}
$site = Get-SPSite -Identity $str
if($site -eq $null)
{
Write-Host “Enter a valid URL”
return
}
$allweb = $site.Allwebs
foreach($web in $allweb )
{
$web | Get-PublishingPages | select Uri, Title, #{Name=’PageLayout’;Expression={$_.Layout.ServerRelativeUrl}}| Format-List
}
Bit of a shot in the dark here, but have you tried setting the scope to RecursiveAll instead of just Recursive? My understanding was that Recursive only hit all files in a folder while RecursiveAll gets all subfolders as well.
Reference: http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spviewscope.aspx?cs-save-lang=1&cs-lang=csharp#code-snippet-1

Resources