I'm using PowersHell to automate iTunes but find the error handling / waiting for com objects handling to be less than optimal.
Example code
#Cause an RPC error
$iTunes = New-Object -ComObject iTunes.Application
$LibrarySource = $iTunes.LibrarySource
# Get "playlist" objects for main sections
foreach ($PList in $LibrarySource.Playlists)
{
if($Plist.name -eq "Library") {
$Library = $Plist
}
}
do {
write-host -ForegroundColor Green "Running a loop"
foreach ($Track in $Library.Tracks)
{
foreach ($FoundTrack in $Library.search("$Track.name", 5)) {
# do nothing... we don't care...
write-host "." -nonewline
}
}
} while(1)
#END
Go into itunes and do something that makes it pop up a message - in my case I go into the Party Shuffle and I get a banner "Party shuffle automatically blah blah...." with a "Do not display" message.
At this point if running the script will do this repeatedly:
+ foreach ($FoundTrack in $Library.search( <<<< "$Track.name", 5)) {
Exception calling "Search" with "2" argument(s): "The message filter indicated
that the application is busy. (Exception from HRESULT: 0x8001010A (RPC_E_SERVER
CALL_RETRYLATER))"
At C:\Documents and Settings\Me\My Documents\example.ps1:17 char:45
+ foreach ($FoundTrack in $Library.search( <<<< "$Track.name", 5)) {
Exception calling "Search" with "2" argument(s): "The message filter indicated
that the application is busy. (Exception from HRESULT: 0x8001010A (RPC_E_SERVER
CALL_RETRYLATER))"
At C:\Documents and Settings\Me\My Documents\example.ps1:17 char:45
If you waited until you you had a dialog box before running the example then instead you'll get this repeatedly:
Running a loop
You cannot call a method on a null-valued expression.
At C:\Documents and Settings\Me\example.ps1:17 char:45
+ foreach ($FoundTrack in $Library.search( <<<< "$Track.name", 5)) {
That'll be because the $Library handle is invalid.
If my example was doing something important - like converting tracks and then deleting the old ones, not handling the error correctly could be fatal to tracks in itunes.
I want to harden up the code so that it handles iTunes being busy and will silently retry until it has success. Any suggestions?
Here's a function to retry operations, pausing in between failures:
function retry( [scriptblock]$action, [int]$wait=2, [int]$maxRetries=100 ) {
$results = $null
$currentRetry = 0
$success = $false
while( -not $success ) {
trap {
# Set status variables at function scope.
Set-Variable -scope 1 success $false
Set-Variable -scope 1 currentRetry ($currentRetry + 1)
if( $currentRetry -gt $maxRetries ) { break }
if( $wait ) { Start-Sleep $wait }
continue
}
$success = $true
$results = . $action
}
return $results
}
For the first error in your example, you could change the inner foreach loop like this:
$FoundTracks = retry { $Library.search( "$Track.name", 5 ) }
foreach ($FoundTrack in $FoundTracks) { ... }
This uses the default values for $wait and $maxRetries, so it will attempt to call $Library.search 100 times, waiting 2 seconds between each try. If all retries fail, the last error will propagate to the outer scope. You can set $ErrorActionPreference to Stop to prevent the script from executing any further statements.
COM support in PowerShell is not 100% reliable. But I think the real issue is iTunes itself. The application and COM model wasn't designed, IMO, for this type of management. That said, you could implement a Trap into your script. If an exception is raised, you could have the script sleep for a few seconds.
Part of your problem might be in how $Track.name is being evaluated. You could try forcing it to fully evaluate the name by using $($Track.name).
One other thing you might try is using the -strict parameter with your new-object command/
Related
Highly influenced by other questions here on Stackoverflow I have ended up with this method for starting processes from my Powershell-scripts
function global:system-diagnostics-processstartinfo {
[CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='Low')]
param
(
[Parameter(Mandatory=$True,HelpMessage='Full path to exectuable')]
[Alias('exectuable')]
[string]$exe,
[Parameter(Mandatory=$True,HelpMessage='All arguments to be sent to exectuable')]
[Alias('args')]
[string]$arguments
)
if (!(Test-Path $exe)) {
$log.errorFormat("Did not find exectuable={0}, aborting script", $exe)
exit 1
}
$log.infoFormat("Start exectuable={0} with arguments='{1}'", $exe, $arguments)
$processStartInfo = New-Object System.Diagnostics.ProcessStartInfo($exe)
$processStartInfo.FileName = $exe
$processStartInfo.RedirectStandardError = $true
$processStartInfo.RedirectStandardOutput = $true
$processStartInfo.UseShellExecute = $false
$processStartInfo.Arguments = $arguments
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $processStartInfo
$log.info("Start exectuable and wait for exit")
$p.Start() | Out-Null
#$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$log.infoFormat("exectuable={0} stdout: {1}", $exe, $stdout)
$log.debugFormat("exectuable={0} stderr: {1}", $exe,$stderr)
$global:ExitCode = $p.ExitCode
$log.debugFormat("exectuable={0} Exitcode: {1}", $exe, $p.ExitCode)
return $stdout
}
Pretty straight forward with some added logging etc. And it works in all my current use cases execpt one. I have created a script that copies the database dump for our production instance of Confluence to our test server. Then it uses the above method to drop existing database, all fine. But the actual restore just hangs for ever and ever. So right now I have to exit the script and then run the following command manually
d:\postgresql\bin\pg_restore.exe -U postgres -d confluencedb -v -1 d:\temp\latest-backup.pgdump
It takes some time and there is quite a lot of output. Which makes me belive that there must be either one the following causing the issue
The amount of output makes a buffer overflow and stalls the script
It takes to much time
Anyone with similar experiences who can help me resolve this. It would enable to schedule the import, not having to do it manually as today.
I had to do the following right after process. Start:
# Capture output during process execution so we don't hang
# if there is too much output.
do
{
if (!$process.StandardOutput.EndOfStream)
{
[void]$StdOut.AppendLine($process.StandardOutput.ReadLine())
}
if (!$process.StandardError.EndOfStream)
{
[void]$StdErr.AppendLine($process.StandardError.ReadLine())
}
Start-Sleep -Milliseconds 10
}
while (!$process.HasExited)
# Capture any standard output generated between our last poll and process end.
while (!$process.StandardOutput.EndOfStream)
{
[void]$StdOut.AppendLine($process.StandardOutput.ReadLine())
}
# Capture any error output generated between our last poll and process end.
while (!$process.StandardError.EndOfStream)
{
[void]$StdErr.AppendLine($process.StandardError.ReadLine())
}
# Wait for the process to exit.
$process.WaitForExit()
LogWriteFunc ("END process: " + $ProcessName)
if ($process.ExitCode -ne 0)
{
LogWriteFunc ("Error: Script execution failed: " + $process.ExitCode )
$FuncResult = 1
}
# Log and display any standard output.
if ($StdOut.Length -gt 0)
{
LogWriteFunc ($StdOut.ToString())
}
# Log and display any error output.
if ($StdErr.Length -gt 0)
{
LogWriteFunc ($StdErr.ToString())
}
I have a script that tests whether one of our websites is working properly.
Test 1: It checks the HTTP status. (no problem here)
Test 2: It checks whether user authorization is working because we have had issues with this in the past.
Then it waits one hour and runs both tests again.
The overall structure of the script is as follows (pseudocode):
while (1 -eq 1) {
test1
if ($result -eq "pass") {
test2
}
start-sleep -seconds 3600
}
Test 2 is where the problem occurs. How this test works is: navigate to the login page, enter credentials, click login button. Check URL, as successful login leads to a different URL from the login page. This works fine, except on (seemingly random) loops it cannot access the elements of the page: the username and passwords fields and the login button. The exceptions that get thrown vary depending on how I change the code around to try to fix the problem.
Some error messages I've gotten include:
System.Runtime.InteropServices.COMException The RPC server is
unavailable. (Exception from HRESULT: 0x800706BA)
System.Runtime.InteropServices.COMException
The property 'value' cannot be found on this object. Verify that the property exists and
can be set.
System.Runtime.InteropServices.COMException
OperationStopped: (:) [], COMException
HResult: -2147352319
No such interface supported
System.Runtime.InteropServices.COMException
OperationStopped: (:) [], COMException
HResult: -2147352319
This command is not supported.
The offending code is anything that tries to access a page element with getElementByID, for example:
$ie.Document.getElementByID("username").value = $userame
Again, the weird thing is that the code runs fine for hours and hours... but then, usually in the middle of night, it randomly becomes unable to access the page elements.
Following is the full code of Test 2 in case that helps:
$ie = New-Object -ComObject "internetExplorer.Application"
#$ie.Visible = $true
$ie.navigate2($loginPage)
while ($ie.busy -eq $true) {start-sleep -seconds 1}
$ie.Document.getElementByID("username").value = $username
$ie.Document.getElementByID("password").value = $password
$ie.Document.getElementByID("login-button").click()
while ($ie.busy -eq $true) {start-sleep -seconds 1}
if ($ie.Document.url -eq $landingPage) {
#success
} else {
#failure
}
$ie.quit()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($ie) | out-null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
$process = get-process -name "iexplore"
while ($process) {
try {
stop-process -name "iexplore" -ea stop
} catch [Microsoft.PowerShell.Commands.ProcessCommandException] {
#do nothing
}
try {
$process = get-process -name "iexplore" -ea stop
} catch [Microsoft.PowerShell.Commands.ProcessCommandException] {
$process = $null
}
}
Until I can figure out what the problem is, I've implemented a workaround. I put the offending code in a try block. In the catch block, I restart the script. Following is not exactly how it is implemented in my script but it gives you the idea:
try {
$ie.Document.getElementByID("username").value = $username
} catch {
start powershell {$scriptPath}
exit
}
When the script restarts, the elements of the page can once again be accessed with getElementByID.
This tool has 2 possible options;
Reset 1 Access Point
Reset ALL Access Points at a site
For the sake of record keeping, I have a function that sends an email alert when either of these events occur.
Reset a single AP:
Function Manage-APReset {
Write-Verbose "Function start: Manage-APReset"
Write-Host "Executing access point reset for $apName .."
IF($controllerName -eq $null) {
Error-NoCon }
else {
## Establish connection(s)
[string]$cUser = "srv-****"
$cPassword = ConvertTo-SecureString -String "X***********" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential ($cUser, $cPassword)
Write-Host "Establishing SSH connection to Cisco Controller $controllerName"
New-SSHSession -ComputerName $controllerName -Credential $cred
$session = Get-SSHSession -Index 0
$stream = $session.Session.CreateShellStream("PS-SSH", 0, 0, 0, 0, 100)
sleep 4
Write-Host "Connected. Authenticating for SSH Stream.."
## Invoke login
$stream.WriteLine('srv-*****')
sleep 3
$stream.WriteLine('X********j8')
sleep 2
$Stream.Read()
Write-Host "Authenticated!"
## Invoke commands
$stream.WriteLine("config ap reset $apName")
sleep 2
$stream.WriteLine('y')
sleep 3
$stream.Read()
Write-Host "$apName has been reset successfully. Please allow up to 5 minutes to come back up"
Admin-SendAlert($event = 1)
Remove-SSHSession -SessionId 0,1,2,3,4,5,6,7,8,9
Repeat
}
}
You'll notice at the end I call Admin-SendAlert which handles the email alert. During this time I pass $event = 1 to allow Admin-SendAlert to know what condition is occurring.
Function Admin-SendAlert {
Write-Verbose "Function started: Admin-SendAlert"
## Event 1: Single AP Reset Successfully
if($event = 1) {
$eventSub = "Single: $apName has been reset"
$eventBod = "$apName has been reset successfully by $tech`n Reason Summary: $reasonSum"
}
if($event = 2) {
$eventSub ="Full Store Reset: $Store All APs Reset"
$eventBod = "The following APs have been reset at Store $Store by user $tech. `n`nAll APs:`n $apArray`n Reason Summary: $reasonSum"
}
Send-MailMessage -To "CSOC <blank#email.com>" -From "AP Manager Beta <srv-blank#email.com>" -Subject $eventSub -SmtpServer smtp.email.com -Body $eventBod
}
I don't believe this is how this should be handled as the value of $event remains whichever comes first. How should I be doing this?
Your main issue is that you are using the assignment operator in place of the equality comparator. So if($event = 1) should be if($event -eq 1)
I see room for improvement as well for you if clauses. You are checking the numerical value of $event. It will only be one of those values. Never two. Your if clauses are mutually exclusive yet you attempt to evaluate both -eq 1 and -eq 2. Not the best idea as it make for muddy code. You should be using if and elseif to contain it in the same block
if($event -eq 1) {
# Stuff happens
} elseif($event -eq 2) {
# Stuff happens
} else {
# Last resort
}
Further to that, if yourself with too many elseif clauses, you would be better off using switch
switch($event){
1 {
# Stuff happens
break
}
2 {
# Stuff happens
break
}
default {
# Last resort
}
}
Note: if you don't use break it will evaluate all switch conditions and execute all that match.
I'm having a bit of trouble preventing a certain error message from bubbling up from a function to my main routine's 'Catch'. I would like to have my function react to a particular error, then do something, and continue processing as usual without alerting my main routine that there was an error. Currently, if the file this script is trying to read is in use (being written to), it will write a System.IO.IOException error to my log. But sometimes I expect this error to occur and it isn't an issue and I don't want to fill my log with these type of errors. I would expect from the code below that the checkFileLock function would catch the error, return 0 to the findErrorInFile function, and no error would be caught to my error log.
Function findErrorsInFile{
param(
[string]$dir,
[string]$file,
[String]$errorCode
)
If((Get-Item $($dir + "`\" + $file)) -is [System.IO.DirectoryInfo]){ #we dont want to look at directories, only files
}Else{
If($(checkFileLock -filePath $($dir + "`\" + $file))){
$reader = New-Object System.IO.StreamReader($($dir + "`\" + $file))
$content = $reader.ReadToEnd()
$results = $content | select-string -Pattern $errorCode #if there is no regex match (no matching error code found), then the string $results will be == $null
If($results){
Return 1 #we found the error in the file
}Else{
Return 0 #no error found in the file
}
}Else{
Return 0 #The file was being written to, we will skip it and assume no error. This is rare.
}
}
}
Function checkFileLock{
param(
[String]$filePath
)
try{
$openFile = New-Object System.IO.FileInfo $filePath
$testStream = $openFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) #try to open a filestream
If($testStream){ #If the filestream opens, then it isn't locked
$testStream.Close() #close the filestream
}
return $false #File is not locked
}
catch{
return $true #File is locked
}
}
#### START MAIN PROCESS ####
Try{
if($(findErrorsInFile -dir 'somepath' -file 'somefilename' -errorcode 'abc')){
write-host "found something"
}else{
write-host "didn't find anything"
}
}
Catch{
$_.Exception.ToString() >> mylogfile.txt
}
try/catch blocks only catch terminating errors. Is your code generating a terminating or non-terminating error?
ArcSet has highlighted essentially what is required: force a non-terminating error to be a terminating error. I suspect it needs to be added to this line, if allowed:
$testStream = $openFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) -ErrorAction Stop
Edit - solution as -ErrorAction not an accepted parameter
I tried the above and it's not allowed. An alternative is to set the $ErrorActionPreference to Stop. This will affect all errors, so recommend reverting. Someone with more experience using System.IO.FileInfo objects may have a more elegant solution.
try{
$currentErrorSetting = $ErrorActionPreference
$ErrorActionPreference = "Stop"
$openFile = New-Object System.IO.FileInfo $filePath
$testStream = $openFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) #try to open a filestream
If($testStream){ #If the filestream opens, then it isn't locked
$testStream.Close() #close the filestream
}
$ErrorActionPreference = $currentErrorSetting
return $false #File is not locked
}
catch{
$ErrorActionPreference = $currentErrorSetting
return $true #File is locked
}
Use to force a catch
-ErrorAction Stop
use to suppress a error
[Command with error] | out-null
I noticed that if applying a configuration through Start-DscConfiguration fails, it writes to the error stream but doesn't
throw an Exception? That is, if I do the following:
try{
Start-DscConfiguration -Path ".\MyConfig" -Wait -Verbose
}catch{
#...
}
...it never ends up in the catch handler. I suspect this may have something to do with the fact that without the "-Wait",
Start-DscConfiguration starts an async job for this, and async commands probably don't throw exceptions, but in a synchronous
scenario, I would very much like to know if my configuration could be applied.
What is the proper way to determine if Start-DscConfiguration has completed succesfully?
The only way I know is to check the global "$error" variable and compare the number of error records before and after your call to Start-DscConfiguration. If there's more afterwards then something must have gone wrong during the call, so throw your own exception:
Configuration TestErrorHandling {
Node "localhost" {
Script ErroringResource {
GetScript = { return $null; }
TestScript = { return $false; }
SetScript = { throw new-object System.InvalidOperationException; }
}
}
}
$errorCount = $error.Count;
write-host "starting dsc configuration"
$mof = TestErrorHandling;
Start-DscConfiguration TestErrorHandling –Wait –Verbose;
write-host "dsc configuration finished"
if( $error.Count -gt $errorCount )
{
$dscErrors = $error[$errorCount..($error.Count - 1)];
write-host "the following errors occurred during dsc configuration";
write-host ($dscErrors | fl * | out-string);
throw $dscErrors[-1];
}
There's another way to make it cause an exception. Try saving it into the ErrorVariable like this :
try
{
Start-DscConfiguration -Path ".\MyConfig" -Wait -Verbose -ErrorVariable ev
}
catch
{
$myException = $_
}
Weirdly so, this throws the exception when there's an error (which is what you wanted). You can get the value of your exception in the $myexception variable, and also could get just a one liner description of your error using $ev
PS: Note that while mentioning ev in the errorVariable parameter, you do it without the '$' symbol - since you're only specifying the variable 'name'.
Start-DscConfiguration when used without -Wait will create a job object - with one child job for every computername. PowerShell job objects have an Error stream which contains all the errors. You can check this stream as well
$job = Start-DscConfiguration -Force -Verbose -Path C:\Temp\Demo\ -ComputerName localhost
Receive-Job $job -Wait
'Errors in job = ' + ($job.childjobs[0].Error.Count)