Exploring iTunes Using PowerShell: Part 1

I don’t spend a ton of time using iTunes on my laptop, but when I do it is usually to listen to some music or the iTunes radio and even that isn’t a common thing. But I happened to come across some information about a COM object within iTunes. Knowing this, I was able to connect to iTunes and perform some actions against it such as viewing the iTunes object, playing a file as well as manipulating the volume and a couple of other things.

Of course, you will need to have iTunes installed on your computer. Once you have done that it is just a matter of creating the connection to iTunes using the itunes.application COM object.

#Create iTunes Object
$itunes = New-Object  -ComObject iTunes.Application

iTunes will open up after you do this, so don’t be alarmed when the application all of a sudden pops open. Besides that, we can still start to explore the object and see what we can find.

image

Quite a few things here that are worth exploring to see what they hold. We can tell from the Version property that I am running iTunes Version 11.1.1.11.

We can play a file by using PlayFile() and supplying the full path to the file.

#Play a file
$itunes.PlayFile("C:\temp1 Head Like A Hole.m4a")

We can then view the current song information by looking at the CurrentTrack object.

#Show currently playing song information
$itunes.CurrentTrack

image

Lots of nice information! Everything that you could find by digging into the song file in iTunes can be found here for use. And as you can tell, a lot of this information can be edited if you see fit.

image

If you saw on the picture above this one, you would see Nine Inch Nails classified as Electronica/Dance (really?). So lets make a small change to what it really should read as.

$itunes.CurrentTrack.Genre = 'Industrial Rock'

image

image

Ok, lets, see if this is currently playing a song by checking the player state property. If it returns a 0, then that means that there is nothing playing (stopped or paused) while a 1 means that there is something currently playing at this moment.

#Player State; 0 -> Ready to play; 1 -> Currently Playing
$itunes.PlayerState

image

Looks like we still have a song playing; maybe I should pause this for a second so I can finish this example.

#Pause Song
$itunes.Pause()

image

Looks to not be playing now. Ok, now lets continue playing the song.

#Play
$itunes.Play()

Maybe I want the volume a little lower. To do this, I make use of the Volume property and give it a value of 0-100. I’ll drop it down to 75 in this case.

#Adjust Volume
$itunes.SoundVolume
$itunes.SoundVolume = 75
$itunes.SoundVolume

image

Cool! We can also mute and un-mute the volume using the Mute property and supplying a boolean value of True (Mute) or False (Un-mute).

#Mute iTunes
$itunes.Mute = $True

#UnMute iTunes
$itunes.Mute = $False

Lets skip to the next song now.

#Play Next track 
$itunes.NextTrack()
$itunes.CurrentTrack

image

If I wanted to go back a song, I simply do the following:

#Play previous track 
$itunes.PreviousTrack()

The last thing that I will show you is how to subscribe to a podcast and then update the subscription.

#Subscribe to a podcast
$iTunes.SubscribeToPodcast("http://feeds.feedburner.com/PowerScripting") 
 
#Update Podcast         
$iTunes.UpdatePodcastFeeds() 

I check my Podcasts folder in iTunes and sure enough, there is my new subscription.

image

Before I forget, you can quit iTunes using the Quit() method.

$itunes.Quit()

Being that this is a COM object, there is always the case for the memory to be released after use, so we will just do a little cleanup to ensure everything is good to go.

$null = [System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$itunes)
[gc]::Collect()
Remove-Variable itunes

As I continue to read the documentation on the SDK for the iTunes COM object and explore the object, it has become painfully obvious that not everything I want is publicly exposed for consumption. Even though, there is still more to cover in Part 2 as I will show you how to explore your playlists and play items from there as well as checking out what devices are currently connected to iTunes and adjusting the equalizer. So until then, thanks for stopping by!

Posted in powershell | Tagged , , | 8 Comments

Using $PSDefaultParameterValues in PowerShell

Recently I saw someone posted a reply to one of my comments on a blog article that he had never heard of $PSDefaultParameterValues before now.  With that, I will give you a walkthrough of this feature added in PowerShell V3 and show you some thing that you can do with it!

$PSDefaultParameterValues is an automatic variable that exists in PowerShell and the object type is System.Management.Automation.DefaultParameterDictionary. Basically, this is a hash table that you can add a Name/Value combination to the existing object. However, unlike a typical hash table, there are requirements to adding items to this object.

There are 3 possibly syntaxes that are allowed with this:

  • $PSDefaultParameterValues=@{“<CmdletName>:<ParameterName>”=”<DefaultValue>”}
  • $PSDefaultParameterValues=@{“<CmdletName>:<ParameterName>”={<ScriptBlock>}}
  • $PSDefaultParameterValues[“Disabled”]=$true | $false

Note that just using the top two syntaxes will overwrite the current object. If you wish to add things to the existing object, you have to use the Add() method instead.

  • $PSDefaultParameterValues.Add({“<CmdletName>:<ParameterName>”,”<DefaultValue>”})

Removing an item is as easy as using the .Remove() method.

  • $PSDefaultParameterValues.Remove(“<CmdletName>:<ParameterName>”)

Lastly, you can clear out everything in this object by calling the .Clear() method.

$PSDefaultParameterValues.Clear()

You can also Enable or Disable the object globally, meaning that everything will either work (Enabled) or not work (Disabled).

#Disable
$PSDefaultParameterValues.Add("Disabled", $true)
#OR
$PSDefaultParameterValues["Disabled"] = $true
#Enable
$PSDefaultParameterValues.Add("Disabled", $false)
#OR
$PSDefaultParameterValues["Disabled"] = $false

Ok, now that we have covered the syntaxes and being able to enable/disable this as well, it is time to dive in and show some examples.

Let’s say we want to make sure that we do not accidently stop any processes using Stop-Process. We can force WhatIf to always be used with Stop-Process to better protect ourselves:

$PSDefaultParameterValues.Add("Stop-Process:WhatIf",$True)

image

Ok, lets try this out.

Get-Process | Stop-Process

image

Perfect! Instant safety net to avoid any potential issues.

Heck, we can take it one step further by forcing all cmdlets and functions to force WhatIf:

$PSDefaultParameterValues.Add("*:WhatIf",$True)

 

image

Now anything that utilizes WhatIf will not actually make any changes.

If I wanted a little more security, but not outright blocking my ability to perform an action, then I could just set Confirm to $True.

$PSDefaultParameterValues.Add("*:Confirm",$True)

image

One of the last cool things you can do is specify a script block that can run different default values based on a certain condition, such as using Format-Table.

$PSDefaultParameterValues=@{"Format-Table:AutoSize"={if ($host.Name -eq "ConsoleHost"){$true}}}

Other things that could prove to be useful are adding Verbose output on specific cmdlets, setting up an some parameters for Send-MailMessage. A personal favorite is using some saved credentials to automatically supply them on any cmdlet that has a Credential parameter.

I would recommend that if you have favorite or common values for parameters that you use, you should make some additions to you PowerShell profile to include them.

Some of the values that I use in my PowerShell profile:

$Cred = Get-Credential
$PSDefaultParameterValues.Add("*:Credential",$Cred)
$PSDefaultParameterValues.Add("Get-ChildItem:Force",$True)
$PSDefaultParameterValues.Add("Receive-Job:Keep",$True)
$PSDefaultParameterValues.Add("Format-Table:AutoSize",{if ($host.Name -eq "ConsoleHost"){$true}})
$PSDefaultParameterValues.Add("Send-MailMessage:To","<emailaddress>")
$PSDefaultParameterValues.Add("Send-MailMessage:SMTPServer","mail.whatever.com")
$PSDefaultParameterValues.Add("Update-Help:Module","*")
$PSDefaultParameterValues.Add("Update-Help:ErrorAction","SilentlyContinue")
$PSDefaultParameterValues.Add("Test-Connection:Quiet",$True)
$PSDefaultParameterValues.Add("Test-Connection:Count","1")

I am always interested in seeing what others are using, so feel free to share what you are using with $PSDefaultParameterValues!

Posted in powershell, V3, V4 | Tagged , , , | 4 Comments

Give PowerShell a Voice Using The SpeechSynthesizer Class

Besides using PowerShell for the usual work related things, we can also have some fun with it by tapping into various .Net classes to do things such as letting PowerShell talk to us. There are quite a few examples of this floating around and this is my turn at talking about it and showing what you can do as well as showing off one of my functions. This is also one of many articles that I have had on the backburner while working things that I felt were more important to post, but felt like I had better get this out before I wait much longer.

Back in the day, one would have looked towards using a COM object (sapi.spvoice) to make this happen. But I won’t be touching that in this article but instead will be looking at the System.Speech.Synthesis.SpeechSynthesizer class instead. At the end I will point you towards a small function that I created which will allow you to easily send data to PowerShell which will then be spoken to you.

Lets start digging into this by creating an object using the SpeechSynthesizer class. But before that, we need to add the System.Speech assembly as it is not already loaded.

Add-Type -AssemblyName System.speech

Now we can create the SpeechSynthesizer object.

$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer

image

Exploring the object, we can see that there are a few properties and many methods in this object.

$speak | Get-Member

image

To simply use the defaults and say something , do this using the Speak() method:

$speak.Speak("Would you like to play a game?")

Using the Speak() method blocks the console until everything has finished. If you want to free up the console as soon as you supply the string, use the SpeakAsync() method instead.

You can even save your data to a WAV file so it can be played at a later time. Before you begin using Speak() or SpeakAsyn(), first call the SetOutputToWaveFile() and supply a string showing the file path with file name.

$speak.SetOutputToWaveFile("C:\users\Administrator\Desktop\test.wav")
$speak.Speak("Would you like to play a game?")
$speak.Dispose()

Don’t forget call Dispose() after your final call to Speak(), otherwise you will not be able to play the WAV file.

image

Now that we know how to let PowerShell speak to us, lets take a look at a few things we can do to adjust settings prior to using the Speak() methods.

The first property you can look at is Volume. It does exactly what you would think: adjusts the volume that will be used when speaking. The accepted values are 0 –> 100; anything more or less will result in a error being displayed. Also note that the default value of this is 100.

Rate is another property which can easily be adjusted based on your preference. The accepted values are –10 –> 10. The default value at the time of object creation is 0. Dipping into the negative values will slow the voice down while proceeding towards 10 will speed the voice up. Give it a shot and have some fun!

By default, you get the great sound of Microsoft David Desktop talking to you when use want PowerShell to speak to you.

$speak.voice

image

Depending on your OS installation and possibly other things, this may be the only voice that you have access to. I am currently running Windows Server 2012 R2 on this system and have access to other voices. You can do the following to see what voices you have available:

$speak.GetInstalledVoices()

image

Well, I know that we have 3 voices, but this doesn’t provide much useful information as it what kind of voices, so lets dig deeper.

$speak.GetInstalledVoices().VoiceInfo

image

Much better! Well, besides David I have two female voices available to use. Lets go ahead and try the next in line: Microsoft Hazel Desktop. I can do this by using the SelectVoice() method and providing the full name of the voice.

$speak.SelectVoice('Microsoft Hazel Desktop')

image

And now we can check out the new voice:

$speak.Speak('Would you like to play a game?')

Excellent! We now have a different voice that can be used instead of Dave.

Now if you were thinking that this might be a great function, well then you are correct! I went ahead and put together a function called Out-Voice that will take pipeline input or data via the –InputObject parameter to depending on the other parameters, either speak to you or send the data to a WAV file instead.

First I will dot source the script to load the function.

. .\Out-Voice.ps1

And now I will have it speak the current time:

"The current time is $((Get-Date).ToShortTimeString())" | Out-Voice

You could even use this to run a scan on something and have it say something to you when it has completed. There are quite a few possibilities that you can use with this. Go out and explore!

Currently, I do not have support for the other voices, but they are planned for a future update. But until then, the function in its current form is available for download at the link below. Give it a shot and let me know what you think!

Download Out-Voice

http://gallery.technet.microsoft.com/scriptcenter/Out-Voice-1be16d5e/file/104052/1/Out-Voice.ps1

Posted in powershell | Tagged , , , | 7 Comments

Checking Out The Where and ForEach Operators in PowerShell V4

#Requires –Version 4.0

With the release of PowerShell V4, we received the Desired State Configuration (DSC) module as the main piece as well as some other things, some known and some not known that well. One of those things is the use of Where() and ForEach() operators (both added to support DSC) on an object, similar to the example below:

$services = Get-Service
$services.where()
$services.ForEach()

Ok, that really doesn’t show you the power that is in these two items (especially Where()).  So lets step into using Where() and try to do some filtering on some services.

$services = Get-Service
$services.where({$_.Name -match '^w'})

image

Ok, nothing we haven’t seen before. Instead of piping the objects into Where-Object, we use a dot notation to call Where and supply a script block to it and we get the same results. So what is the big deal? Well, there is another parameter that you can set after the scriptblock to get some very interesting results. 

The documentation for this is well, lacking. So in order to really what it is looking for, you can force an error to occur and it will show you the parameters that are expected on this.

$services.Where()

image

Here we see the following syntax is expected:

.Where({ expression } [, mode [, numberToReturn]])

Basically, you have to supply a System.Management.Automation.WhereOperatorSelectionMode enum and another optional parameter after that. What are the possible values that I can use? Lets find out!

[enum]::GetNames('System.Management.Automation.WhereOperatorSelectionMode')

image

Default

Default filtering like using Where-Object

First

Display the First item in the filtered collection; allows optional integer to specify more items to show (First 3)

Last

Display the Last item in the filtered collection; allows optional integer to specify more items to show (Last 3)

SkipUntil

Display the filtered results while only showing the n number of objects after the initial object being filtered (this can include non-filtered items) only after it finds a match

Until

Display the objects in the collection until the expected results of the filter

Split

Splits results filtered and with the remaining results in separate collections; an optional parameter can specifiy an integer that will split out the first n number of filtered results with the rest of the filtered results being dropped into the second collection regardless if they match the criteria or not.

Now we have defined all of these options, so now lets put them to some use and get a better idea as to what we are looking at. Note: All of these will use the $services=Get-Service object to keep everything consistent unless otherwise noted.

Default

This is your standard filtering of data. It will only show you what was filtered without anything else special happening.

$services.where({$_.Name -match '^r'})
# OR
$services.where({$_.Name -match '^r'},'Default')

image

This verifies that we have essentially duplicated what Where-Object already does. So lets now move on the next item and see what we can do.

First

Now we start to get into the fun stuff! Just by calling First after the script block, it will return 1 item from the collection by default.

$services.where({$_.Name -match '^r'},'First')

image

Now lets find the first 3 items in my filtered match.

$services.where({$_.Name -match '^r'},'First',3)

image

Last

Similar to using First, I will show you how to get the last item in the collection that is filtered using the Where() operator.

$services.where({$_.Name -match '^r'},'Last')

image

Here you see that the last item in the collection is displayed. Just like using First with an additional count of just how many to display, we can do the same with Last.

$services.where({$_.Name -match '^r'},'Last',3)

image

SkipUntil

This is where it starts getting interesting. SkipUntil brings something different to the game when used with the Where() operator. I am also going to switch up and use a range of integers (1..20) to better show this one and Until. Basically, what happens is that when you use SkipUntil with your filter, it will display the results of the filter up until the number you specify. If you specify nothing for the 3rd parameter, then it will display everything including anything that would not have normally been matched via the filter. First lets look at this using a number to determine returned results.

@(1..10).Where({$_ -eq 2},"skipuntil",2)

 

image

Here you see that we returned the match of the filter, 2 as well as another value that is after the matched value in the collection.

If you just want that match, then do this.

@(1..10).Where({$_ -eq 2},"skipuntil",1)

image

Now with nothing specified for the 3rd parameter.

@(1..10).Where({$_ -eq 2},"skipuntil")

image

You can see that it not only displays 2, but the rest of the values afterwards. So what happens if no match is found based on the filter, will it display everything or nothing?

@(1..10).Where({$_ -eq 14},"skipuntil")

image

If you guessed nothing, then you are correct! This only works if there is a match found on the filter, otherwise nothing else will happen.

Until

Until works in the opposite direction of SkipUntil. By that I mean that it will return values up until the matched object in the collection and will not include that match in the returned results.

@(1..10).Where({$_ -eq 5},"until")

image

As you can see, everything up until the 5 is displayed based on the match. If I want, I can choose to only display the first 2 values.

@(1..10).Where({$_ -eq 5},"until",2)

image

Split

I really saved the most interesting, in my opinion, for last. Using Split will actually return 2 separate collections of objects: the first containing everything that matched the filter and the second containing everything that was filtered out if used without any parameter to specify a number of objects to return.

$services = Get-Service
$results = $services.where({$_.Name -match '^r'},'split')

Ok, I have my $results variable filled with the collections. Now lets see the first item in that collection.

$results[0]

image

Here is everything that matched my filter. So if that is in this collection, then that must mean everything else is in the other collection…

$results[1]

image

image

Everything before the match and after the match was stored in the other collection. Pretty cool stuff!

We can also specify a number of those matches to be in the first collection and then discard everything else into the other collection with the excluded objects.

$results = $services.where({$_.Name -match '^r'},'split',2)
$results[0]
$results[1]

image

The first two items of that match in the collection and…

image

everything else is in the second collection.

Performance

There are performance gains from using the Where() operator over piping everything to Where-Object as well. This is due to you having everything loaded up into memory vs. running it through the pipeline. The following examples show various uses of Where against similar Where-Object examples:

$where_operator = (1..20 | ForEach {
    Measure-Command {
         (1..1E4).Where({$_ -gt 777})
    }
} | Measure-Object -Property TotalMilliseconds -Average).Average

$whereobject = (1..20 | ForEach {
    Measure-Command {
         (1..1E4) | Where {$_ -gt 777}
    }
} | Measure-Object -Property TotalMilliseconds -Average).Average

$where_operator_first = (1..20 | ForEach {
    Measure-Command {
         (1..1E4).Where({$_ -gt 777},'first')
    }
} | Measure-Object -Property TotalMilliseconds -Average).Average

$whereobject_first = (1..20 | ForEach {
    Measure-Command {
         (1..1E4) | Where {$_ -gt 777} | Select -First 1
    }
} | Measure-Object -Property TotalMilliseconds -Average).Average

$where_operator_last = (1..20 | ForEach {
    Measure-Command {
         (1..1E4).Where({$_ -gt 777},'last')
    }
} | Measure-Object -Property TotalMilliseconds -Average).Average


$whereobject_last = (1..20 | ForEach {
    Measure-Command {
         (1..1E4) | Where {$_ -gt 777} | Select -Last 1
    }
} | Measure-Object -Property TotalMilliseconds -Average).Average

Write-Verbose "Time measured in milliseconds" -Verbose
[pscustomobject]@{
    where_operator = $where_operator
    whereobject = $whereobject
    where_operator_first = $where_operator_first
    whereobject_first = $whereobject_first
    where_operator_last= $where_operator_last
    whereobject_last = $whereobject_last
}

image

You can see here that using the Where operator does have the edge in each of the examples.

ForEach

The ForEach() operator is also new to PowerShell V4 and like Where(), does have performance perks vs. its pipeline counterpart yet slower than using ForEach () {}.

$foreach_operator = (measure-command {@(1..1E6).ForEach({$_})}).TotalMilliseconds
$foreach_pipeline = (measure-command {@(1..1E6) | ForEach{$_}}).TotalMilliseconds
$foreachobject_pipeline = (measure-command {@(1..1E6) | ForEach-object{$_}}).TotalMilliseconds
$foreach_ = (measure-command {ForEach ($i in @(1..1E6)) {$i}}).TotalMilliseconds

Write-Verbose 'Results in milliseconds' -Verbose

[pscustomobject]@{
    foreach_operator = $foreach_operator
    foreach_pipeline = $foreach_pipeline
    foreachobject_pipeline = $foreachobject_pipeline
    foreach_ = $foreach_
} | Format-List

image

As with Where(), you can find out what it is expecting in parameters by doing the following;

@(1..10).ForEach()

image

Unlike the where operator, I have had little success in getting the expression along with the arguments to work properly. As I explore this some more, I will try to find some good examples as to how you can use these.

One thing that you can do is cast the object like this:

@(1..10).ForEach([double])

image

We can verify that this actually happened by first seeing this done without the casting…

Get-Member -input @(1..10).ForEach({$_}) | Select TypeName -first 1

image

… with one that does the casting.

Get-Member -input @(1..10).ForEach([double]) | Select TypeName -first 1

image

Pretty neat stuff!

Posted in powershell, V4 | Tagged , , , , | 15 Comments

Automatically Declining Itanium Updates in WSUS using PowerShell

Working with WSUS, I sometimes find myself declining the exact same type of updates each month after Patch Tuesday. Not surprisingly, a common trend of those updates involve having the word ‘’Itanium” somewhere in the update title.  Instead of having to mess with clicking through the UI and finding all of these annoying updates, and as there are no automated declining rules available in WSUS, I think a more automated approach is necessary using the APIs and PowerShell.

Honestly, there really isn’t a whole lot of code required to make this happen. The key piece is that you need to have the WSUS Administrator console installed (for Win2K8R2 and below) or the UpdateServices module available (Win2K12 and above).

I will break down the code below and at the very end have all of it available to copy to create. My idea behind this is to just make a scheduled job that can be used to run every second Wednesday (after Patch Tuesday) to automatically decline all of these unneeded updates.

Param (
    [string]$UpdateServer = 'DC1',
    [int]$Port = 80,
    [bool]$Secure = $False
)

This is my main parameters that are configurable based on your environment. Here I decide server I will connect to, the port that is open on the server and if the connection is secure.

If (-Not (Import-Module UpdateServices -PassThru)) {
    Add-Type -Path `
"$Env:ProgramFiles\Update Services\Api\Microsoft.UpdateServices.Administration.dll" -PassThru
} 

A check is made for the UpdateServices module by actually attempting to import the module. I call –Passthru so an object is outputted which can then be used in the If statement. If no object is outputted, then it is assumed that the module doesn’t exist, the OS doesn’t support the module, etc… and then attempts to load the assembly from a known good location.

$Wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::`
GetUpdateServer($UpdateServer,$Secure,$Port)

Finally, a connection is made to the WSUS server using the parameters specified.

image

$approveState = 'Microsoft.UpdateServices.Administration.ApprovedStates' -as [type]
 
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope -Property @{
    TextIncludes = 'itanium'
    ApprovedStates = $approveState::NotApproved,
    $approveState::LatestRevisionApproved,
    $approveState::HasStaleUpdateApprovals
}

image

Here I begin to piece together the filter that will be used with the query for updates. I create an object that holds the ApprovedStates enumeration which will make it easier to find what I can use with the creation of the UpdateScope object. Speaking of the UpdateScope object, this is the filtering piece that I will include with my query. I make sure that I specify itanium as for the TextIncludes propery as well as the ApprovedStates. Because I want to make sure that all non-approved and all non-declined updates have been found, I add the necessary ApprovedStates to the object.

$wsus.GetUpdates($updateScope) | ForEach {
    Write-Verbose ("Declining {0}" -f $_.Title) -Verbose
    $_.Decline()
}

image

Lastly, I can now finally begin declining all of these updates that have been found by my query. Fortunately, all I need to do is call the Decline() method associated with each update object.

You can download the source code for this below and modify it to suit your needs before deploying it in your environment as a scheduled job!

In my case, I will use PowerShell to create my scheduled job.

$triggerParam = @{
    At = "12:00 AM"
    DaysOfWeek = 'Wednesday'
    WeeksInterval = 1
    Weekly = $True
}
$trigger = New-JobTrigger @triggerParam
$JobParam = @{
    Trigger = $trigger
    FilePath = 'C:\DeclineItanium.ps1'
    Name = 'DeclineItaniumUpdates'
}
Register-ScheduledJob @JobParam

image

image

Source Code

Param (
    [string]$UpdateServer = 'DC1',
    [int]$Port = 80,
    [bool]$Secure = $False
)

If (-Not (Import-Module UpdateServices -PassThru)) {
    Add-Type -Path "$Env:ProgramFiles\Update Services\Api\Microsoft.UpdateServices.Administration.dll" -PassThru
} 

$Wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::`
GetUpdateServer($UpdateServer,$Secure,$Port)
 
$approveState = 'Microsoft.UpdateServices.Administration.ApprovedStates' -as [type]
 
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope -Property @{
    TextIncludes = 'itanium'
    ApprovedStates = $approveState::NotApproved,
    $approveState::LatestRevisionApproved,
    $approveState::HasStaleUpdateApprovals
}
 
$wsus.GetUpdates($updateScope) | ForEach {
    Write-Verbose ("Declining {0}" -f $_.Title) -Verbose
    $_.Decline()
}
 
Posted in powershell, WSUS | Tagged , , | 9 Comments