23 January 2014

A little while ago I had a dilemma brought to me at work from an application team.  One of the tings we have fought using the Dragon Naturally Speaking application is the loss of data or profile corruption due to the application not being properly shut down.

The solution provided to us by a third party vendor was bloated and seemed much more complex than it needed to be.  An AutoIT script was written by someone in my company to use the files provided by the vendor.  It was the AutoIT script that called a batch file, which called cscript to run a vbs file (which did the saving), then went back to the AutoIT to call a com file which called an executable (created by one of the vendors) to close Dragon.  Did you get lost?  I was for a while trying to figure out what the scripts were doing and why.

It’s a simple process and the execution should be simple as well (although in IT that is often not the case).  So, I set out to find a way to do it.  Upon inspecting the COM I noticed it pointed to DragonCloser.exe which appeared to be a custom executable that the third party vendor wrote (or maybe Nuance did…).  So, there must be an API to use.  I looked it up and found an SDK online but I was hesitant to use it because one of my goals was not to have to push additional files to all of the PC’s that use Dragon.  I also didn’t want to modify the scripted package I wrote to install Dragon.

After taking another look at the VBS script I noticed it was creating an object without ever loading an assembly or any library of any sort.  This was the clue I needed to know that all I really needed was already loaded on the PC’s.  I dove into the Program Files directory and found Interop.DNSTools.dll. Interop obviously standing for Interoperability and that was exactly what I was looking to do.  I opened up the library in visual studio and browsed for the function I found in the VBS script.  Now I knew what I needed to do.

The resulting Powershell script is only 7 lines.  You can view/download it here:  SaveAndCloseDragon

It’s fully commented and shouldn’t be too hard to follow.  I hope this helps anyone looking to solve this issue.  You can set it as a logoff script on any computer running Windows Vista or higher.  I’m not sure if XP can do Powershell scripts or if installing Powershell adds that functionality to the logoff scripts area.


23 September 2013

Boy, I really need to fix the comments formatting on my posts :\ #notime


7 August 2013

Wow, it’s been 5 months since I posted here!  A lot has been going on with work, I took a real vacation, and I’m not so single anymore.  I definitely have a ton of material to blog about, especially in the Powershell realm (go figure…).  Like the script I wrote that will do subnet discovery (IP, PC name, logged in user) with just the input of a single PC.  I’ve also been meaning to post stuff I wrote much longer ago like how I do my logging, how I put everything together in a single console, and maybe even a fun one: “How to send a speech message to another Windows 7 machine :).”  I feel a little bad though… I haven’t touched PS much at all the past few months due to projects at work not leaving me time to play with it as much since they have me going into other realms like Citrix.

Today, I’m going to talk about a little PHP I wrote from scratch to dynamically create a photo gallery on a web page.  I’m building one for my photography and I want the site to be LOW MAINTENANCE!  This means no having to jump into code every time I want to add something or update it.  The only other time I’ve dabbled in PHP is when I created this blog’s theme from absolute scratch.  The code for the image gallery is simple, and I’m sure I could have googled for some code, but I would be much of a Curious Geek if I didn’t try and figure it out on my own.  Since I’m not really a PHP programmer and I don’t know much, I just googled on how to do specific tasks like “PHP list directory” or “PHP dynamically insert html into page.”  Found the functions I needed pretty quick and as a result I have this:

<?php $dirlist = scandir('./Portraits'); ?>

This is the third line of code, below the DOCTYPE and my PHP include (my header for all of my pages).  This is the line of code that actually scans a given directory (In this case, the directory “Portraits” inside the same directory as the php page itself, they both are in the root).  If I were putting the PHP file in the same directory as the photos, it would just be a “.”.  The end of that sentence looks weird…. I assign it to the variable $dirlist so I can reference it later when I’m actually inserting the html.

I guess I should explain the layout of my gallery.  I have the main photo being viewed on the left, and the thumbnails of all the images in the directory on the right of that.  My rows are only 3 thumbnails wide.  I use CSS to set the sizes and layout, which I wont get into here.

Next, we have:

<div class="photoBox" id="photo1">
	<img id="displayImage" src="./Portraits/<?php echo $dirlist[2] ?>">
	<div id="displayCaption" class="caption captionFade"></div>
</div>

This is the div that contains the displayed photo.  The only thing I really want to note here is the insertion of php code into the img src URL.  I want it to start out displaying the first image it finds (the first two values in the array are . and ..).

Now here is the code doing the brunt of the work:

<div class="thumbContainer">
	<?php
		foreach ($dirlist as $imgname) {
			If ($imgname !== "." and $imgname !== ".." and strpos($imgname, 'txt') == false) {  
				$filename = substr_replace($manipulate, "txt", -3, 3); 
				$filepath = './Portraits/' . $filename;
				If (file_exists($filepath)) {
					$innerHTML = file_get_contents($filepath);
				}
				Else {
					$innerHTML = " ";
				}
				$htmlString = '<div class="thumb"><img src="./Portraits/' . $imgname . '" onmousedown="document.getElementById(\'displayImage\').src=\'./Portraits/' . $imgname . '\'; displayCaption.innerHTML=\'' . $innerHTML . '\'"></div>';
				echo $htmlString;				
			}
		}
	?>
</div>

This is the container for the thumbnails, where the end-user clicks images to dynamically change the page.

  • Lets start with the ForEach line where I begin the loop.  I’m iterating through each of the values in the array that was returned from listing the contents of the directory.
  • On the next line, I’m using an If statement to determine if the value is “.” (current directory), “..” (up a directory/parent directory), or contains .txt in the file name (since my caption files with image information are also here, and obviously aren’t images so they shouldn’t be displayed or tried to load as an image).  strpos is a function to get the position of text in a string.  If the text doesn’t exist, it will either return the boolean false or return 0, which will evaluate to false.  If all of those criteria are met…
  • I derive the name of the txt file (the image and text file have the same name, just different extensions) from the name of the image file by replacing the last three characters of the name with “txt” and set that to $filename
  • Next, I assemble the entire (relative) path of where the text file should be and assign it to $filepath
    • The reason I don’t use the contents of the array for this is that not all images will have a text file, so this was a bit easier than writing logic to check the array again for the existence…
  • Then, I check to see if that file actually exists with the next If statement
  • If it does exist, I set $innerHTML to the contents of that text file, otherwise I set the variable to just a space (blank when it shows up).
  • I then assemble the entire html string that will create the thumbnail, it’s link, and the javascript that will dynamically change the photobox on the page.  You can see where I use my variables $imgname and $innerHTML to dynamically set the values of the img src’s and the caption text that may or may not accompany an image.
  • Then I write that to the page with a simple echo command
  • And then the ForEach loop moves on to the next file that was returned in the array to do the next image…

Pretty simple stuff I think.  It didn’t take me long to write it (if you subtract the time it took me to get quotes and php syntax all fixed…).  This way, when I want to add an image and optionally a caption all I have to do is use FTP to upload them to the proper directory… AND THAT’S IT!!  No having to jump into code for every little update!

Thanks to the official PHP 5 manual for telling me how to use a couple functions 🙂


2 March 2013

EDIT:  This is my old, kludgey way of doing it.  Please see this new post:  Remotely Enabling WinRM – Improved!

*********************

This post is going to build off of the technique I used in my previous post.  My company only recently deployed Powershell to all of our XP devices.  But neither the XP nor Windows 7 have WinRM enabled by default.  This limits me so I had to seek a way to enable it on the fly.  In the past when I’ve needed to run commands that weren’t “remote-friendly” in Powershell I had to create a scheduled task on the PC to run a batch script.  I figured that was the way I’d need to go with this so I set out to do it.  The tricky part was getting Powershell to run elevated on the Win 7 machines, running as System does not necessarily do that.  With the help of the last post’s discovery I was able to accomplish it, though.  Now I can remotely gather data without interfering with the user!  They say the perfect IT guy is the one that accomplishes his work without the end-user even knowing what’s going on.

I’ve decided to just start attaching my completed script file to make it easier, and it’s commented so it should explain what I’m doing well enough.  I’ve also thrown my info header at the top of the script so that maybe I can get some exposure if these things get passed around. This script requires that you have administrative rights on the remote machine!

In short, what it does is write .bat files to the PC, sets scheduled tasks to run them, runs them manually, and then deletes the .bat files and tasks.

I also want to note the reason I have -skipnetworkprofilecheck added to the bat file inside the script.  This can raise a security concern since it’s going to enable the firewall rule for not only domain and private networks but also public!  If I don’t do this, enable-psremoting will fail on some of our PC’s that run virtual environments such as VMWare (which my group, and some others, use for testing).  The VMWare network adapter (and probably VirtualBox and others are like this) are set as public networks.  If enable-psremoting detects a public network it will NOT do the enabling, even with the -force command.  You’ll see later on in the .ps1 script where I modify the firewall rule to remove public networks.

Ok, here is the script:  EnablePSRemoting

**I haven’t tested this actual script.  The one I use at work is integrated into my console and some of the code in this is actually separate functions in my console.  I’m pretty sure it works though 😉


28 February 2013

I searched far and wide over the internet looking for an easy way to elevate powershell in a script whenever I needed to.  The only solutions I ever found were practically a full page of code to look at this, and check that, then do this, jump through this hoop so it could do that… It had to be simpler than that!  I didn’t even attempt to use their solutions because I refused to accept it’s complicated code.

It happens with start-process.  The parameters are picky in how they are used but once you straighten it out in your head (or through many different trial and error iterations like myself) it’s not too bad.  You can eleveate from within powershell, from a command prompt, or from a shortcut.

From Powershell:

start-process powershell.exe -verb runAs

Or if you want to run a command:

start-process powershell “get-process” -verb runAs
Anything passed in the quotes after the process name is passed as an argument to powershell (kind of like a script block)

You can also pass the path to a script as the argument.

From the Command Line

powershell.exe “start-process powershell -verb runAs”
Same thing as in powershell except you start a normal shell and execute a command to open an elevated shell right off the bat.

From a Shortcut

Your target should be:  C:\path-to-powershell\powershell.exe “optional script file path” -verb runAs

And that’s all there is to it.  Now, if you have UAC turned on you will be prompted.  At work I don’t have this problem because we have a man-in-the-middle software that injects the token that allows the elevation without a UAC prompt.


23 January 2013

Finally got tired of all the spam that shows up in my comments awaiting approval (all the activity I get, lol) so I implemented a plugin to to make sure the commenter is human.

Simple enough.


21 January 2013

A couple of days after I made my last post I realized that, for that specific purpose, I could have just dynamically created hashtables as the values of each array.  It would look like this pseudo-code:

$array = @()
$array += @{
   DriveLetter = $object.DriveLetter
   TotalSpace = $object.Size
   FreeSpace = $object.FreeSpace
}

Just remember to first declare the array variable as an array or you’ll get errors.  Then as I enumerate through each returned object it’s as simple as:

ForEach ($disk in $array) {
  write-host "Drive: $disk.DriveLetter has $disk.FreeSpace free space out of a total of $disk.TotalSpace"
}

Or whatever I wanted to do with it.  Now that I think about it, creating a specific name for each variable isn’t necessary and I can’t think of any scenarios where I really need to do that.  I guess it could happen, though, so I’ll leave my previous post up.


4 January 2013

I was building a more involved function the past couple of days for gathering data from a PC in the enterprise should troubleshooting be necessary.  The type of company I work for doesn’t always allow us to stay on the PC and troubleshoot an item (unless it’s a total blocker) so being able to gather a broad range of data is necessary to really troubleshoot since we can’t be on the machine.  Part of that function is retrieving hard drive information.  I’m only gathering a few pieces of info, but since PC’s can have multiple drives I needed a good way to manage each disk separately.  I could have done it with the following line of code, but as you can see from the screenshot, it’s messy and the numbers output are in bytes which isn’t useful at first glance.

gwmi -class win32_logicaldisk | Where {$_.DriveType -eq 3 -OR $_.DriveType -eq 4} | select -property DeviceID,Size,FreeSpace

gwmi

 

The big thing is to convert those numbers into something more meaningful.  I also don’t like the clumped output and wanted to be able to handle each disk separately and call any of it’s properties without additional formatting (to see what I mean, run that code and then try pulling just $var.DeviceID and you’ll see all the hashtable @ and{} characters… it’s a mess).  I decided to create the hashtable myself.  Since there could be multiple disks I also needed it to really be more like an object.  That’s where nesting my hashtables came in.  My other requirement, dynamically creating variables, comes from wanting to return all of my unknown-beforehand-number-of-disks within the same variable, but needing a way to make unique hashtable key names ($return.Disk1… , $return.Disk2… , etc).  It sounds confusing, so here’s the code:

Function getHD($strComputer) {
	# Retrieves hard drive information
	$return = @{}  # declare our return variable as a hashtable so it doesn't get created as a string
	$i = 0  # counter and dynamic part of variable for disks
	$hd = gwmi -comp $strComputer -class win32_logicaldisk
	ForEach ($disk in $hd) {
		If ($disk.Drivetype -eq 3 -OR $disk.DriveType -eq 4) {   # only return physical hard drive and network drive types (5 is CD)
			$i++
			$return.numDrives = $i # where I store the number of drives found so I can use that to iterate through them all later
			$return += @{
				"Disk$i" = @{ # new hashname must be in quotes so variable will be expanded
				DriveLetter = $disk.DeviceID
				Size = $disk.Size/1048576   # Convert to MB
				FreeSpace = $disk.FreeSpace/1048576   # Convert to MB
				}
			}
		}
	}
	return $return
}

 

  1. First, I declare my return variable as a hashtable so that if I wanted to store a certain bit of info at the root of it, it wouldn’t be a string.
  2. Next I declare the variable (a counter) that will be the dynamic part of my nested hashtable key name.
  3. Get the actual info, as objects with properties.
  4. Start my loop
  5. I only want physical hard disks and network drives to return, so I’m limiting what objects I actually look at based on their DriveType.  3 is physical, 4 is network.
  6. Note I’m not starting to count up on my counter until I have a match of a disk type I want.  This keeps my numbers continuous, with no gaps in the numbering for the final output.
  7. Next I’m setting my hashtable key that stores the value of the number of drives returned
  8. Now I’m adding a new (nested) hashtable to my $return hashtbale.  I do this by using the += to say I’m adding to and not replacing my hashtable, and then saying that what I’m adding is actually a hashtable itself by folling the += with the @{
  9. Now, inside the hashtable declaration, I’m creating the dynamic hashtable itself.  Note that the hashtable name is in quotes, if I didn’t use the quotes powershell would error.  Putting it in quotes causes the $i to expand to whatever it’s value is.  Thus, for each iteration it changes:  Disk1 = @{…}, Disk2=@{…}, etc
  10. Then the next few lines are me setting the key names for each disk, example:  Disk1.DriveLetter = $disk.DeviceID
    1. You can’t do this symply by typing out Disk$i.DriveLetter = $disk.DeviceID. Reason being the Disk$i is not in quotes and you can’t use . notation if it is in quotes.  (This does not work:  “Disk$i”.DriveLetter = whatever)
    2. The math in there is just me converting bytes to MB… I know, nowadays what drives aren’t in GB but that’s easy to see from MB.
  11. And then finally I return the entire $return hashtable.

So now what?  I call my function by:  $hdinfo = getHD $pc      Then I run through that returned hashtable “object” and output the data to a file.  That is just a few simple lines:

For ($i=1; $i -le $hdInfo.numDrives;$i++) {
     $hdvar = "`$hdinfo.disk$i"
     invoke-expression $hdvar | Out-file $global:consolePath\PCData\$pc\PCVitals.txt -append -noclobber
}

 

  1. I’m starting a for loop that starts at 1 (Disk1) and will keep adding up until I get to the the number of drives that I stored in that key numDrives.
  2. Next I have to dynamically retrieve that dynamically named variable.  To do that I have to set a variable to be the name of the variable since I have to expand again.  Putting the ` infromt of $hdinfo keeps it from expanding that part.  I want to keep that part of the name literal since I dont want it’s value yet.  Since I know the exact number and how the naming convention goes, I just use my $i counter and put it in the part where it belongs:  disk$i.  The resulting value of $hdvar ends up being “$hdinfo.disk1” (2,3,4, etc).  But it’s a string value, not the object/hashtable itself.  that’s why….
  3. In the last line I’m calling invoke-expression because it’s a cmdlet that tells powershell to take the contents of the variable passed to it and treat it like it’s a command that was typed at the prompt (i.e. it will convert the string to a command).  And I’m pipping it out to a file.
    1. The other thing that wont work is piping your hashtable directly to the out-file.  Your resulting file will end up just having System.Whatever.Hashtable or something like that as it can’t figure out how to convert the output so it just outputs the object type.

And what does it all look like?

Name                           Value                                                                                   
----                           -----                                                                                   
FreeSpace                      1171586.5703125                                                                         
DriveLetter                    C:                                                                                      
Size                           1437513.9921875                                                                         

Name                           Value                                                                                   
----                           -----                                                                                   
FreeSpace                      1143174.359375                                                                          
DriveLetter                    D:                                                                                      
Size                           1423843.99609375                                                                        

Name                           Value                                                                                   
----                           -----                                                                                   
FreeSpace                      1034453.83984375                                                                        
DriveLetter                    E:                                                                                      
Size                           1430727.99609375

I could do a little more formatting if I wanted, but this is good enough for me!  And if I wanted to further use that data in a function it’s easy to reference an individual item with just $hdinfo.Disk1.FreeSpace or $hdinfo.Disk2.Size


20 November 2012

I need to vent a little before I get into the code.  Had a frustrating one today!  All because of unclear documentation in MSDN’s published library of their .NET classes. I guess it’s true what they say, that sometimes you should listen to what isn’t said.

I had to build a work-around to a problem with an MSI from a certain cardiovascular imaging software.  It seems the MSI wants to strip the permissions for the local “Users” group off of the C:\ProgramData folder.  Not cool!  At first the vendor tried to brush it under the rug and gave me a few lines about the development process, blah, blah (develoment?!  you’re just fixing the installer!) and then said it could take 10-12 weeks (which is further out than our very-rushed go-live) and if I could just work around it.  Did I mention this isn’t a lesson in customer service skills?

So anyway, I sat down to figure out how I was going to do it.  I’ve lost a lot of my VB skill because I haven’t used it in over 6 months and I’ve been learning a lot of Powershell.  I love Powershell and I’ve already written code to modify permissions (on a file on XP) so I figured this would be easy.  The one good thing I had going is the issue is only affecting Windows 7 so running the Powershell script locally isn’t a problem (we never pushed PS out to our XP devices).  What I thought would be a 5 minute mod of my existing code turned out to be about 30-40 minutes.

It seems just supplying the user/group (IdentityReference),  read/write/etc (FileSystemRights), and Allow/Deny (AccessControlType) were enough… but not really.   Mine was set to:

 

$Ar = New-Object  system.security.accesscontrol.filesystemaccessrule("Users","ReadAndExecute","Allow")

When setting just those three properties the permissions got applied but if you right-clicked on the folder and viewed the Security tab of the folders properties you saw a blank list of rights for that user/group.  All except for “Special Permissions” at the bottom.  If you went into advanced and dug a little deeper you could see that the Traverse folder/ execute file, List folder/read data, Read attributes, and Read extended attributes were all selected.  Read permissions was not.  Kind of a problem.  Below is what that looked like.

The other problem is it says “Apply to:  Subfolders and files only”.  With the changes not applying to the root folder I actually made the changes on, and not having proper read permissions, I have a funky looking ACL.  The odd thing is that the permissions I was wanting to apply worked.  Someone who fell into the “Users” group was able to read and execute like I wanted.  But, it’s technically broken so it needed to be fixed.

So I called up the MSDN library to find out what else I might need.  This post is already longer than it needs to be so it will suffice to say that I fiddled around with the InheritanceFlags Enumeration and PropagationFlags Enumeration parameters for quite a while.  I assumed the the FileSystemRights could be comma-separated since there were many options and you could have multiple.  I tried that and it worked, I got the “Read Permissions” checkbox back on the advanced properties view.   Still, the ACL was blank on the Security tab (with the exception of Special Permissions).  that took setting the Inheritance and Propagation flags correctly.  Both of these flags apply ONLY to the child object.  They do not affect whether the folder you are making changes to will inherit from it’s parent.  Inherit determinse whether the child object(s) inherit from the folder you are making changes on and propagate is what it pushes them down to.  Propagate is dependent on Inherit.

Ok, finish this up already.  Since I needed to put permisions back to default so that All Users/Public can appropriately access that folder I needed both file and folders to inherit.  And the kicker (reading what isn’t said…) is that the Propagation flags have to be set to None.  Why?  Because it doesn’t mean it wont propagate, it means it’s going to use the default value for propagation, which is:  This folder, subfolders, and files!  But the documentation does not say that anywhere!  It finally dawned on me that, that is what it would do.  So, my final code for adding “Users” to the ACL for the C:\ProgramData folder with Read and execute, List folder contents, and Read permissions is:

 

$Acl = Get-Acl C:\ProgramData
$Ar = New-Object  system.security.accesscontrol.filesystemaccessrule("Users","ReadAndExecute,Read","ObjectInherit,ContainerInherit","None","Allow")
$Acl.SetAccessRule($Ar)
Set-Acl C:\ProgramData $Acl

Line 1 I’m getting the ACL of the object I want to modify
Line 2 I’m creating a new access rule and setting all of the parameters.  The local Users group, the TWO FileSystemRights, the Inheritance Flags (so that folders and files inherit), the Propagation flag (none means use default), and finally that I”m Allowing access and not Denying.
Line 3 I’m taking that access rule and adding it to the ACL object I created from the folder
Line 4 I’m actually setting the new ACL [in stone]

Yes, all that for 4 lines of code.

“Special” thanks to the MSDN library:  http://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemaccessrule.aspx


16 November 2012

Bleh!  I’ve been so busy with the new position I started in April I’ve fallen off posting on this blog.  I have a lot of Powershell stuff I want to post—creating progress/activity bars, hashing, and perhaps a series on creating a simple text console.  I’ve also not been paying attention to replies, sorry!  I get so much spam from this blog that Gmail started dumping all my email alerts to comments in my spam box, which I never check.  I did respond to a couple, though… and realized I REALLY need to build-out the styling of the reply section and add the functionality to reply to comments!

Anyway, I just wanted to drop this quick and easy one here that I had to figure out last night.  I tried googling a solution for batch conversion last night when I needed to convert about 40 flv’s to mp4.  The web yielded nothing too great.  One site had a Powershell script but it didn’t work.  So, I just took from it what parameters I had to pass to the VLC application and wrote my own script.

This script has to be run from the directory where the videos are and it outputs them to the same directory.  I’m using Powershell 2.0 on Windows 7 Ultimate with VLC 2.0.4

$fileItems = Get-childitem -filter *.flv

ForEach ($file in $fileItems) {
$destination = “$($file.fullname).mp4”
start-process “C:\Program Files (x86)\VideoLAN\VLC\vlc.exe” “-I dummy -vvv “”$($file.fullname)”” `
–sout=#transcode{vcodec=h264,vb=1024,acodec=mpga,ab=192,channels=2,deinterlace}:standard{access=file,mux=ts,dst=””$destination””} vlc://quit” -wait
}

The parts:

  • $fileItems = Get-childitem -filter *.flv
    • get’s all of the items in the folder that end in .flv (this is why you have to run it in the same folder, I was too lazy that late at night to put in an Open Dialog box 😉 )
  • ForEach ($file in $fileItems) {
    • begins our For loop that basically says “For each file you found in your enumeration results, do the following”
  • $destination =”$($file.fullname).mp4″
    • creates the full-path file name we are SAVING the file as.  I found it necessary to put the full path and set it in a variable because VLC didn’t like it otherwise.  I suspect because the VLC executable is in another directory than the files.  .fullname is a property of the object that is returned that is the fully-qualified path of the file.  If you want to modify anything and just want to use the filename (including extension) then it’s just .name
  • start-process “C:\Program Files (x86)\VideoLAN\VLC\vlc.exe” “-I dummy -vvv “”$($file.fullname)”” ` –sout=#transcode{vcodec=h264,vb=1024,acodec=mpga,ab=192,channels=2,deinterlace}:standard{access=file,mux=ts,dst=””$destination””} vlc://quit” -wait
    • Is technically one line.  The backtick (`) tells Powershell the command continues on the next line.
    • To be honest I don’t know what all the parameters do that are being passed to VLC but I do know the important ones:  vcodec is the name of the video codec you want to use to encode, vb is the video bitrate you want to encode at, acodec is the audio codec you want to encode with, and ab is the audio bitrate you want to encode at.   Channels is the number of audio channels.  dst is the destination file you want to save to.
    • Notice I have $destination surrounded in double quotes.  This is so when powershell actually passes that string to VLC it will put quotes around the destination file string.  This is necessary in the event you have spaces in your path and/or filename itself.  The vlc://quit tells VLC to exit when it’s done transcoding that file.  The -wait tells powershell to not go on to the next one until that current one is complete (lest you crash VLC…

Hope this saves someone some time 🙂