Expert Solutions: Advanced Event 6 of the 2010 Scripting Game


Bookmark and Share


(Note: These solutions were written for Advanced Event 6 of the 2010 Scripting Games.)

 

Advanced Event 6 (Windows PowerShell)


Andy Schneider is a systems engineer who works for Avanade in Seattle, Washington, USA. You can find his blog at http://get-powershell.com.


————


I will be the first to admit that I learned a few things while writing this script. When I saw that it had to respond to USB devices, I immediately started thinking of events and the new support for events in Windows PowerShell 2.0. I decided to work on this part of the code first. Using the WMI Query Language, I came up with this block of code:


$wql = @”

SELECT * FROM __InstanceCreationEvent

WITHIN 2

WHERE TargetInstance

ISA ‘Win32_LogicalDisk’

“@



Let’s break it down, line by line. First, select all the events that have to do with something being created (for example, a new USB drive), and check for it every 2 seconds. Then you filter the events by making sure that what you are responding to (the TargetInstance) is a logical disk. After we have the query, we can register for those events using the Register-WMIEvent cmdlet.


Now that this is all wired up, the next step is figuring out how to do something when this event fires. There is a parameter called –action for Register-WMIEvent which is a ScriptBlock that you can specify. This ScriptBlock will run every time the event fires. There were two problems I came across when working with the –action parameter. The first was, “How do I get at the Win32_LogicalDisk info that caused the event to fire in the first place?” It turns out, within the action ScriptBlock, there are some automatic variables. I was able to find the drive device ID with the following code:


$drive = $event.SourceEventArgs.newEvent.TargetInstance.DeviceID



The second problem was a scoping issue. Within the ScriptBlock, I was not able to reference my variables in my script. Frankly, there is probably a better way to do this, but I resorted to global variables to solve this problem. This is where the $GLOBAL:_xyz variables come from.


The next issue was dealing with how Copy-Item treats files differently than directories. Copy-Item will automatically overwrite files, but it will not automatically overwrite directories unless you use the –force parameter. The logic I wrote to determine what to copy depending on what mode the script is in assumed that I either had a FileInfo object or a DirectoryInfo object. I needed these because my script used the .Name property and the .FullName property on either of these object types.


Rather than putting this logic in the body of the script, I dealt with it during parameter declaration. There is an advanced function parameter attribute called [ValidateScript]. 


param (

[Parameter( Position = 0,

           ValueFromPipeline = $true)]

[Alias(“Directory”)]           

[ValidateScript({($_.GetType().Name -eq “DirectoryInfo”) -or `

                 ($_.GetType().Name -eq “FileInfo”)})]

$file,

 

Basically, I am using the GetType() method to validate that the entries are either fileinfo or directoryinfo objects. This allowed me to specify either a group of files, folders, or any combination of the two. I also used the ValueFromPipeline so that I can pipe a dir into the script. In the Process block, I then gathered up all the pipelined objects into an array so that I can use all of them in my $action ScriptBlock.


Here is the entire script:


<#

    .Synopsis

        Event 6 in the 2010 Scripting Games – Automatically copy files to a USB drive when it is plugged in

   

    .Description

        Requirements:

       

        The Script should allow you to select a file, a group of files, a folder, or a group of folders

        to contain the files that will be copied. The script should offer a “quiet mode” and a “prompt mode” based

        upon a switch supplied to the script when it is launched. Quiet mode should be the default. In quiet mode,

        the script copies the target files/folders to the newly inserted drive without user interaction.

       

        The script should run in continuous loop fashion until it is stopped, or it should offer to make a specific

        number of copies based upon a command-line argument. For example, make five copies of the data, and then stop.

        The script should offer a “force” mode that will overwrite existing files, or a “prompt” mode that will prompt

        before overwriting existing files. This should also be configurable via the command argument.

    

     .Example

         ls *.txt | Event6.ps1

    

     .Notes

         Author: Andy Schneider

         Blog:   http://get-powershell.com          

 

#>

 

param (

[Parameter( Position = 0,

           ValueFromPipeline = $true)]

[Alias(“Directory”)]           

[ValidateScript({($_.GetType().Name -eq “DirectoryInfo”) -or `

                 ($_.GetType().Name -eq “FileInfo”)})]

$file,

 

[Parameter(Position = 2)]

[int]

$timesToCopy = 2,

 

[Parameter(Position = 1)]

[switch]

$promptMode = $false,

 

[Parameter(Position = 3)]

[switch]

$force = $false

)

 

 

Begin {

 

$wql = @”

SELECT * FROM __InstanceCreationEvent

WITHIN 2

WHERE TargetInstance

ISA ‘Win32_LogicalDisk’

“@

 

$GLOBAL:_files = @()

$GLOBAL:_promptMode = $promptMode

$GLOBAL:_timesToCopy = $timesToCopy

$GLOBAL:_force = $force

} # End BEGIN

 

Process {

 

    $GLOBAL:_files += $file

 

} # End Process

 

End {

# Give user some feedback about which files and directories will be copied

Write-Host “The files and directories below will be copied to a USB Drive”

$GLOBAL:_files

write-host “There are $GLOBAL:_timesToCopy copies left until automatic exit – press Ctrl+C to stop”

 

$action = {

    $drive = $event.SourceEventArgs.newEvent.TargetInstance.DeviceID

    $GLOBAL:_timesToCopy -=1;

    write-host “There are $GLOBAL:_timesToCopy copies left until automatic exit”

    foreach ($file in $GLOBAL:_files) {

   

        $fileName = $file.Name

        $fullName = $file.FullName

        $fileExists = Test-Path -path (“$drive$fileName”)

       

        # Note – Copy-Item overwrites files without warning – thus using $fileExists

        if (!($fileExists)) { Copy-Item -Path $fullName -Destination $drive -Recurse}

       

        # Force parameter required if you are copying a directory and you want to overwrite

        # Files will always be overwritten – so it will still work

        if ($_force) {Copy-Item -Path $fullName -Destination $drive -force -Recurse}

       

        if ($_promptMode -and $fileExists) {

            write-warning “Will Overwrite: $drive$fileName”

            Copy-Item -Path $fullName -Destination $drive -force -confirm -Recurse

            }

       

        if ($_promptMode -and (!$fileExists)) {Copy-Item -Path $file -Destination -confirm -Recurse}

             #>  

            } #End foreach

       

    } # End $action

 

# Unregister if we are already subscribed

Unregister-Event newUsbDrive -ErrorAction SilentlyContinue

 

Register-WmiEvent -Query  $wql -SourceIdentifier “newUsbDrive”  -action $action | out-null

 

while ($GLOBAL:_timesToCopy -gt 0) {

    # sit and wait for a USB Drive to be inserted

    sleep -Milliseconds 50

    } # While

   

# Clean up global variable garbage

   

Remove-Variable _files

Remove-Variable _promptMode

Remove-Variable _timesToCopy

Remove-Variable _force

Remove-Variable _promptMode

 

 

 

} # End

Comments (0)

Skip to main content