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



The following image shows the result of running the script.

Image of result of running script

 

 

Advanced Event 6 (VBScript)

Steve Lee, Senior Test Manager VBScript
Windows Management Platform
http://blogs.msdn.com/wmi

 


For this challenge, the goal was to write a script that would be triggered when a USB drive was inserted and copy a selected number of files and folders to that drive. Because the trigger was an operating system event, I immediately thought about finding a WMI event class for notifying when a new drive was available.


WMI has a Win32_VolumeChangeEvent class that fires an event whenever there is any change to the disk volumes. For this specific challenge, I was only concerned when a new drive arrived, so I created a WQL notification query for this purpose: select * from Win32_VolumeChangeEvent where EventType = 2. Based on this WMI class, Values, and ValueMap qualifiers, I can see that a value of 2 means disk arrival, which suits my purpose.


In general, I like to separate getting the command-line arguments (also called parameters) from the main subroutine. In the GetArguments() subroutine, I tried to simplify the number of arguments that would be accepted to the source path, a regular expression for pattern matching, and two switches whether to prompt and overwrite (as required by the challenge).


To keep the script simple, I decided to just use regular expressions. Most scripters are familiar with regular expression. If I had to support basic wildcard characters, I would have written a function to convert a wildcarded string into a regular expression.


After I have parsed all the arguments, the main function is relatively simple. I start an asynchronous WMI event notification query, and the script basically sleeps from there. For this particular challenge, I could have used a simpler semi-sync notification query, but in general, async is preferred because it allows my script to process further and is a pattern I’ve become accustomed to even when not necessary.


My SINK_OnObjectReady() subroutine is called when WMI has an event that matches my filter. In here, I prompt if needed to verify that the user wants to copy the files to this particular USB drive. When confirmed, I wrote my own file copy routine primarily for the purpose of showing progress. I could have used the VBScript CopyFolder method, but it would have been less user-friendly because no progress information is given. Writing my own routine also allows me to satisfy the bonus goals of checking free space and listing files to be copied, although I did not actually do that here.


The file copy subroutine is pretty straight forward. I iterate through the files in the source folder and use the regular expression to check for matches, and finally copy the file. After that, I recursively go through the subfolders.


Here is the entire script:

doPrompt = false

doOverwrite = false

sourceFolder = ""

pattern = "*"

set wmiSink = WScript.CreateObject("wbemscripting.SWbemSink","SINK_")

set fso = CreateObject("scripting.filesystemobject")

set shell = CreateObject("wscript.shell")

 

call GetArguments()

call Main()

 

sub Main()

      set cimv2 = GetObject("winmgmts:root/cimv2")

      call cimv2.ExecNotificationQueryAsync(wmiSink, "Select * from Win32_VolumeChangeEvent " & _

            "where EventType = 2")

      wscript.echo "Waiting for new USB Drive..."

      while (true)

            wscript.sleep 1000

      wend

end sub

 

sub CopyFilesTo(source, dest)

      wscript.echo "Copying files from " & source & " to " & dest

      set regex = new RegExp

      regex.pattern = pattern

 

      set srcFolder = fso.GetFolder(source)

      for each srcFile in srcFolder.Files

            if (regex.Test(srcFile.name)) then

                  wscript.echo "Copying " & srcFile.Path

                  On Error Resume Next

                  call srcFile.Copy(dest, doOverwrite)

                  if (err) then

                        wscript.echo "Error: " & err.description

                  end if

                  On Error Goto 0

            end if

      next

      for each subFolder in srcFolder.SubFolders

            destFolder = dest & subFolder.Name

            if (regex.Test(subFolder.name)) then

                  if (not fso.FolderExists(destFolder)) then

                        wscript.echo "Creating folder " & destFolder

                        fso.CreateFolder (destFolder)

                  end if

                  call CopyFilesTo(subFolder.path, destFolder & "\")

            end if

      next

end sub

 

sub SINK_OnObjectReady(obj, ctx)

      if (doPrompt) then

            input = ""

            while (input <> "y" and input <> "n")

                  wscript.echo "Copy files to " & obj.DriveName & " (Y/N)?"

                  input = lcase(WScript.stdin.ReadLine())

            wend

            if (input = "n") then

                  exit sub

            end if

      end if

      call CopyFilesTo(sourceFolder, obj.DriveName & "\")

      wscript.echo "Done" & vbnewline & "Waiting for new USB Drive..."

end sub

 

sub GetArguments()

      set args = WScript.Arguments

      if (args.Count < 2) then

            ParamError("<source> and <pattern> needs to be specified")

      elseif (args.Count > 2) then

            for i = 2 to args.Count - 1

                  select case lcase(args(i))

                  case "-prompt"

                        doPrompt = true

                  case "-overwrite"

                        doOverwrite = true

                  case else

                        ParamError("Unknown option " & args(i))

                  end select

            next       

      end if

      sourceFolder = WScript.Arguments(0)

      pattern = WScript.Arguments(1)

end sub

 

sub ParamError(str)

      WScript.Echo "Error: " & str & vbNewline

      PrintHelp()

      WScript.Quit 1

end sub

 

sub PrintHelp()

      WScript.Echo "Usage: " & WScript.ScriptName & " <source> <regExp> [-prompt] [-overwrite]" & _

            vbNewline & vbNewline & "Ex: " & WScript.ScriptName & " c:\test .*?\.ps1 -overwrite"

end sub

 

The following image shows the results of running the script:

Image of result of running script

 


If you want to know exactly what we will be looking at tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys