PowerShell Memory leak misunderstanding - powershell

New to PowerShell, so kind of learning by doing.
The process I have created works, but it ends up locking down my machine until it is completed, eating up all memory. I thought I had this fixed by looking into forcing the garbage collector, and also moving from a for-each statement to using %() to loop through everything.
Quick synopsis of process: Need to merge multiple SharePoint log files into single ones to track usage across all of the companies' different SharePoint sites. PowerShell loops through all log directories on the SP server, and checks each file in the directory if it already exists on my local machine. If it does exist it appends the file text, otherwise it does a straight copy. Rinse-repeat for each file and directory on the SharePoint Log server. Between each loop, I'm forcing the GC because... Well because my basic understanding is the looped variables are held in memory, and I want to flush them. I'm probably looking at this all wrong. So here is the script in question.
$FinFiles = 'F:\Monthly Logging\Logs'
dir -path '\\SP-Log-Server\Log-Directory' | ?{$_.PSISContainer} | %{
$CurrentDir = $_
dir $CurrentDir.FullName | ?(-not $_.PSISContainer} | %{
if($_.Extension -eq ".log"){
$DestinationFile = $FinFiles + '\' + $_.Name
if((Test-Path $DestinationFile) -eq $false){
New-Item -ItemType file -path $DestinationFile -Force
Copy-Item $_.FullName $DestinationFile
}
else{
$A = Get-Content $_.FullName ; Add-Content $DestinationFile $A
Write-Host "Log File"$_.FullName"merged."
}
[GC]::Collect()
}
[GC]::Collect()
}
Granted the completed/appended log files get very very large (min 300 MB, max 1GB). Am I not closing something I should be, or keeping something open in memory? (It is currently sitting at 7.5 of my 8 Gig memory total.)
Thanks in advance.

Don't nest Get-ChildItem commands like that. Use wildcards instead. Try: dir "\\SP-Log-Server\Log-Directory\*\*.log" instead. That should improve things to start with. Then move this to a ForEach($X in $Y){} loop instead of a ForEach-Object{} loop (what you're using now). I'm betting that takes care of your problem.
So, re-written just off the top of my head:
$FinFiles = 'F:\Monthly Logging\Logs'
ForEach($LogFile in (dir -path '\\SP-Log-Server\Log-Directory\*\*.log')){
$DestinationFile = $FinFiles + '\' + $LogFile.Name
if((Test-Path $DestinationFile) -eq $false){
New-Item -ItemType file -path $DestinationFile -Force
Copy-Item $LogFile.FullName $DestinationFile
}
else{
$A = Get-Content $LogFile.FullName ; Add-Content $DestinationFile $A
Write-Host "Log File"$LogFile.FullName"merged."
}
}
}
Edit: Oh, right, Alexander Obersht may be quite right as well. You may well benefit from a StreamReader approach as well. At the very least you should use the -readcount argument to Get-Content, and there's no reason to save it as a variable, just pipe it right to the add-content cmdlet.
Get-Content $LogFile.FullName -ReadCount 5000| Add-Content $DestinationFile
To explain my answer a little more, if you use ForEach-Object in the pipeline it keeps everything in memory (regardless of your GC call). Using a ForEach loop does not do this, and should take care of your issue.

You might find this and this helpful.
In short: Add-Content, Get-Content and Out-File are convenient but notoriously slow when you need to deal with large amounts of data or I/O operations. You want to fall back to StreamReader and StreamWriter .NET classes for performance and/or memory usage optimization in cases like yours.
Code sample:
$sInFile = "infile.txt"
$sOutFile = "outfile.txt"
$oStreamReader = New-Object -TypeName System.IO.StreamReader -ArgumentList #($sInFile)
# $true sets append mode.
$oStreamWriter = New-Object -TypeName System.IO.StreamWriter -ArgumentList #($sOutFile, $true)
foreach ($sLine in $oStreamReader.ReadLine()) {
$oStreamWriter.WriteLine($sLine)
}
$oStreamReader.Close()
$oStreamWriter.Close()

Related

Using powershell script with different parameters

I have a script that deletes anything older than a set time. I want to replicate this for other delete jobs with different times and different folders
I am new to Powershell, this script was written with a lot of google assistance
$Minutes=[DateTime]::Now.AddMinutes(-5)
$Timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss"
$Log = "C:\test\logs\_" + $Timestamp + ".log"
Start-Transcript -path $Log -append -Force -NoClobber
try {
function Write-Log($string)
{
$outStr = "" + $Timestamp +" "+$string
Write-Output $outStr
}
Write-Log "------------ Start of Log ------------"
#Write-Log ""
# get all file objects to use in erasing
$files=Get-ChildItem -path 'c:\test\*' -Include *.* -Recurse |
Where-Object{ $_.LastWriteTime -lt $Minutes}
# Remove the file and its folder.
$files |
ForEach-Object{
Write-Log " Deleting File --> $_."; Remove-Item $_.Fullname
}
# output statistics
Write-Output "**********************"
Write-Output "Number of old files deleted: $($files.Count)"
Write-Log "------------- End of Log -------------"
}
catch {
Write-Error -Message "Something bad happened!" -ErrorAction Stop
}
Stop-Transcript
Welcome to PowerShell, and good for you on the web search approach. Yet remember, it is vital that being new to this, that you take some time to ramp up on all the basic before you diving into this space, in order to avoid as much undue confusion, frustration, etc., that you will encounter.
You really need to do this also to understand what you need, and to avoid having / causing catastrophic issues to your system and or your enterprise. Of course, never run any code you do not fully understand, and always list out your goals and address them one at a time to make sure you are getting the results you'd expect.
Live on YouTube, Microsoft Virtual Academy, Microsoft Learn, TechNet Virtual Labs, MS Channel9, leveraging all the videos you can consume; then hit the documentation/help files, and all the no cost eBooks all over the web.
As for ...
I want to replicate this for other delete jobs with different times
and different folders
… this is why functions and parameters exist.
Function Start-DeleteJob
{
[CmdletBinding()]
[Alias('sdj')]
Param
(
$JobTime,
$JobFolder
)
# Code begins here
}
So, spend time researching PowerShell functions, advance functions, and parameters.
Get-Help -Name About_*Functions*
Get-Help -Name About_*Parameters*

Folder permissions script exceeding the time allocated for execution

I have a simple PowerShell v4 script that pulls directory permissions on a hard drive and writes them to a file.
It runs fine 90% of the time, but other times my output file is empty or only partially full. The error log says the "task was terminated due to exceeding the time allocated for execution" which is 1 hour. Normally this task runs in 5 minutes.
Any ideas about what might be causing this and a solution?
$RootPath = "D:"
$Folders = dir $RootPath -recurse | where {$_.psiscontainer -eq $true}
foreach ($Folder in $Folders){
$ACLs = get-acl $Folder.fullname | ForEach-Object { $_.Access }
Foreach ($ACL in $ACLs){
if ($Folder.fullname -NotMatch "superoldhome"){
$OutInfo = $Folder.Fullname + "`t" + $ACL.IdentityReference + "`t" + $ACL.AccessControlType
Add-Content -Value $OutInfo -Path $OutFile
}
}}
Is this running as a Windows task? Maybe it is losing access to the hard drive for some reason? If you really want to use this, I would suggest a few debug lines to make sure it stays connected . Maybe something like:
if (Test-Path $Folder) {}
else {Add-Content -Value "Unable to resolve Path" -Path $OutFile}
If you have that in your loop if that is the problem itll write to the log. Or you can log everything to a file that happens. I forget how to do that off the top of my head, but let me know, I can probably dig that up.
Thank you for all the feedback.
I am experimenting with dumpsec and have high hopes for that.
If it doesn't get me what I want I will try adding some more logging like Matt & Kenny suggested.

Powershell Find and Replace Loop, OutOfMemoryException

I have a working powershell script to find and and replace a few different strings with a new string in thousands of files, without changing the modified date on the files. In any given file there could be hundreds of instances of said strings to replace. The files themselves aren't very large and probably range from 1-50MB (a quick glance at the directory I am testing with shows the largest as ~33MB).
I'm running the script inside a Server 2012 R2 VM with 4 vCPUs and 4GB of RAM. I have set the MaxMemoryPerShellMB value for Powershell to 3GB. As mentioned previously, the script works, but after 2-4 hours powershell will start throwing OutOfMemoryExceptions and crash. The script is 'V2 friendly' and I haven't adopted it to V3+ but I doubt that matters too much.
My question is whether or not the script can be improved to prevent/eliminate the memory exceptions I am running into at the moment. I don't mind if it runs slower, as long as it can get the job done without having to check back every couple of hours and restart it.
$i=0
$all = Get-ChildItem -Recurse -Include *.txt
$scriptfiles = Select-String -Pattern string1,string2,string3 $all
$output = "C:\Temp\scriptoutput.txt"
foreach ($file in $scriptFiles)
{
$filecreate=(Get-ChildItem $file.Path).creationtime
$fileaccess=(Get-ChildItem $file.Path).lastaccesstime
$filewrite=(Get-ChildItem $file.Path).lastwritetime
"$file.Path,Created: $filecreate,Accessed: $fileaccess,Modified: $filewrite" | out-file -FilePath $output -Append
(Get-Content $file.Path) | ForEach-Object {$_ -replace "string1", "newstring" `
-replace "string2", "newstring" `
-replace "string3", "newstring"
} | Set-Content $file.Path
(Get-ChildItem $file.Path).creationtime=$filecreate
(Get-ChildItem $file.Path).lastaccesstime=$fileaccess
(Get-ChildItem $file.Path).lastwritetime=$filewrite
$filecreate=(Get-ChildItem $file.Path).creationtime
$fileaccess=(Get-ChildItem $file.Path).lastaccesstime
$filewrite=(Get-ChildItem $file.Path).lastwritetime
"$file.Path,UPDATED Created: $filecreate,UPDATED Accessed: $fileaccess,UPDATED Modified: $filewrite" | out-file -FilePath $output -Append
$i++}
Any comments, criticisms, and suggestions welcomed.
Thanks
Biggest issue I can see is that you are repeatedly getting the file for every property you are querying. Replace that with one call per loop pass and save it to be used during the pass. Also Out-File is one of the slower methods of outputting data to file.
$output = "C:\Temp\scriptoutput.txt"
$scriptfiles = Get-ChildItem -Recurse -Include *.txt |
Select-String -Pattern string1,string2,string3 |
Select-Object -ExpandProperty Path
$scriptfiles | ForEach-Object{
$file = Get-Item $_
# Save currrent file times
$filecreate=$file.creationtime
$fileaccess=$file.lastaccesstime
$filewrite=$file.lastwritetime
"$file,Created: $filecreate,Accessed: $fileaccess,Modified: $filewrite"
# Update content.
(Get-Content $file) -replace "string1", "newstring" `
-replace "string2", "newstring" `
-replace "string3", "newstring" | Set-Content $file
# Write all the original times back.
$file.creationtime=$filecreate
$file.lastaccesstime=$fileaccess
$file.lastwritetime=$filewrite
# Verify the changes... Should not be required but it is what you were doing.
$filecreate=$file.creationtime
$fileaccess=$file.lastaccesstime
$filewrite=$file.lastwritetime
"$file,UPDATED Created: $filecreate,UPDATED Accessed: $fileaccess,UPDATED Modified: $filewrite"
} | Set-Content $output
Not tested but should be fine.
Depending on what you replacements are actually like you could probably save some time there as well. Test first before running in production obviously.
I remove the counter you had since it appeared nowhere in the code.
Your logging could easily be csv based since you have all the object ready to go but I just want to be sure we are one the right track before we go to far.

Why is my PowerShell script writing blank lines to console?

I have a bit of an odd problem. Or maybe not so odd. I had to implement a "custom clean" for a PowerShell script developed for building some unique configurations for my current project (the whys are not particularly important). Basically it copies a bunch of files from the release directories into some temporary directories with this code:
$Paths = Get-ChildItem $ProjectPath -recurse |
Where-Object { ($_.PSIsContainer -eq $true) -and
(Test-Path($_.Fullname + 'bin\release')) } |
Select-Object Fullname
ForEach ($Path in $Paths)
{
$CopyPath = $Path.Fullname + '\bin\Temp'
$DeletePath = $Path.Fullname + '\bin\Release'
New-Item -ItemType directory -path $CopyPath
Copy-Item $DeletePath $CopyPath -recurse
Remove-Item $DeletePath Recurse
}
And after the build copies it back with:
ForEach ($Path in $Paths)
{
$CopiedPath = $Path.Fullname + '\bin\Temp\'
$DeletedPath = $Path.Fullname + '\bin\Release\'
$Files = Get-ChildItem $CopiedPath -recurse |
where-object {-not $_PSIsContainer}
ForEach ($File in $Files)
{
if(-not (Test-Path ($DeletedPath+$File.Name)))
{
Copy-Item $File.Fullname ($DeletedPath+$File.Name)
}
}
Remove-Item $CopyPath -recurse -force
}
This is pretty clunky and noobish (Sorry, I'm a PowerShell noob drinking from a fire hose), but it works for the purpose and I will clean it up later. However, when it executes the initial copy to the temp directories, it writes a lot of blank lines to the screen, which isn't ideal as I have a message I display while this process is executing to assure our CM doesn't freak out and think it broke, but this message is blown away by the blank lines. Do you know what might be causing this and how I might solve this? I'm using PowerShell 2.0 out of the box and due to the nature of this project I can't upgrade or get any outside libraries. Thanks guys.
If the only thing you're looking to do is clean up the console output, then all you need to do is use the pipeline. You can start the command with [void], which will exclude all information from the pipeline. You can also pipe the whole thing into the Out-Null cmdlet, which will trap all output, except for the lines that don't have output.
The New-Item cmdlet by default returns output to the console on my version of Windows PowerShell (4.0). This may not be true on previous versions, but I think it is... Remove-Item also doesn't return any output, usually. If I were to take a stab, I'd kill output on those lines that use the "Item" noun using one of the methods mentioned above.

Powershell add header record

I have a process in SSIS where I create three files.
Header.txt
work.txt
Trailer.txt
Then I use an Execute Process Task to call my Powershell script. I basically need to take the work.txt file and prepend the header record to it (while maintaining integrity of original values in work.txt) and then append the trailer record (which is generated with total row counts, etc.).
Currently I have:
Set-Location "H:\Documentation\Projects\CVS\StageCVS"
Clear-Content "H:\Documentation\Projects\CVS\StageCVS\CVSMemberEligibility"
Get-Content Header.txt, work.txt, Trailer.txt|out-file "H:\Documentation\Projects\CVS\StageCVS\CVSMemberEligibility" -Confirm
This is fine in testing where I only had 1000 rows, but now that I have 67,000 rows the process takes forever.
I was looking at the Add-Content cmdlet but I can't find an example where it adds the header. Can someone assist with the syntax on going to the first line in the file and then adding the content before that first line?
many thanks in advance!
Just to clarify: I would like to build off the work.txt file. This si where the majority of the data is already, so instead of rewriting it all to a new file, I think a copy would make more sense. So in theory I would create all three files. copy the work file to say workfile.txt . Prepend header to workfile, append trailer to workfile, rename workfile.
UPDATE
This seems to work for the trailer.
Set-Location "H:\Documentation\Projects\CVS\StageCVS"
#Clear-Content "H:\Documentation\Projects\CVS\StageCVS\CVSMemberEligibility"
Copy-Item work.txt workfile.txt
#Get-Content Header.txt, work.txt, Trailer.txt|out-file "H:\Documentation\Projects\CVS\StageCVS\CVSMemberEligibility"
Add-Content workfile.txt -value (get-content Trailer.txt)
UPDATE
Also tried:
Set-Location "H:\Documentation\Projects\CVS\StageCVS"
$header = "H:\Documentation\Projects\CVS\StageCVS\Header.txt"
#Clear-Content "H:\Documentation\Projects\CVS\StageCVS\CVSMemberEligibility.txt"
Copy-Item work.txt workfile.txt
#(Get-Content Header.txt, work.txt, Trailer.txt -readcount 1000)|Set-Content "H:\Documentation\Projects\CVS\StageCVS\CVSMemberEligibility"
Add-Content workfile.txt -value (get-content Trailer.txt)
.\workfile.txt = $header + (gc workfile.txt)
This is something that seems so easy but the reality is that it is not due to the underlying filesystem. You are going to need a file buffer or a temp file or if you are really brave you can look at extending the file and transposing the characters. As this guy did in C#.
Insert Text into Existing Files in C#, Without Temp Files or Memory Buffers
http://www.codeproject.com/Articles/17716/Insert-Text-into-Existing-Files-in-C-Without-Temp
So as it turns out out-file and get-content are not very performance enhanced. I found that it was taking over 5 minutes to run 5000 record result set and write/read the data.
When i researched some different performance options for Powershell I found the streamwriter .NET method. For the same process this ran in under 15 seconds.
Being that my result set in production environment would be 70-90000 records this was the approach I took.
Here is what i did:
[IO.Directory]::SetCurrentDirectory("H:\Documentation\Projects\CVS\StageCVS")
Set-Location "H:\Documentation\Projects\CVS\StageCVS"
Copy-Item ".\ELIGFINAL.txt" H:\Documentation\Projects\CVS\StageCVS\archive\ELIGFINAL(Get-Date -f yyyyMMdd).txt
Clear-Content "H:\Documentation\Projects\CVS\StageCVS\ELIGFINAL.txt"
Copy-Item work.txt workfile.txt
Add-Content workfile.txt -value (get-content Trailer.txt)
$work = ".\workfile.txt"
$output = "H:\Documentation\Projects\CVS\StageCVS\ELIGFINAL.txt"
$readerwork = [IO.File]::OpenText("H:\Documentation\Projects\CVS\StageCVS\workfile.txt")
$readerheader = [IO.File]::OpenText("H:\Documentation\Projects\CVS\StageCVS\Header.txt")
try
{
$wStream = New-Object IO.FileStream $output ,'Append','Write','Read'
$writer = New-Object System.IO.StreamWriter $wStream
#$write-host "OK"
}
finally
{
}
$writer.WriteLine($readerheader.ReadToEnd())
$writer.flush()
$writer.WriteLine($readerwork.ReadToEnd())
$readerheader.close()
$readerwork.close()
$writer.flush()
$writer.close()