Running IIS Express on a Random Port

I have found myself using IIS Express for a bunch of web projects these days, and each of these projects is using different frameworks and different authoring systems. (Like Windows Notepad, which is still the one of the world's most-used code editors.)

Anyway, there are many times when I need multiple copies of IIS Express running at the same time on my development computer, and common sense would dictate that I would create a custom batch file for each website with the requisite parameters. To be honest, for a very long time that's exactly how I set things up; each development site got a custom batch file with the path to the content and a unique port specified. For example:

@echo off

iisexpress.exe /path:c:\inetpub\website1\wwwroot /port:8000

The trouble is, after a while I had so many batch files created that I could never remember which ports I had already used. However, if you don't specify a port, IIS Express will always fall back on the default port of 8080. What this means is, my first IIS Express command would work like the example shown below:

CMD> iisexpress.exe /path:c:\inetpub\website1\wwwroot

Copied template config file 'C:\Program Files\IIS Express\AppServer\applicationhost.config' to 'C:\Users\joecool\AppData\Local\Temp\iisexpress\applicationhost2018321181518842.config'
Updated configuration file 'C:\Users\joecool\AppData\Local\Temp\iisexpress\applicationhost2018321181518842.config' with given cmd line info.
Starting IIS Express ...
Successfully registered URL "http://localhost:8080/" for site "Development Web Site" application "/"
Registration completed
IIS Express is running.
Enter 'Q' to stop IIS Express

But my second IIS Express command would fail like the example shown below:

CMD> iisexpress.exe /path:c:\inetpub\website2\wwwroot
Copied template config file 'C:\Program Files\IIS Express\AppServer\applicationhost.config' to 'C:\Users\joecool\AppData\Local\Temp\iisexpress\applicationhost2018321181545562.config'
Updated configuration file 'C:\Users\joecool\AppData\Local\Temp\iisexpress\applicationhost2018321181545562.config' with given cmd line info.
Starting IIS Express ...
Failed to register URL "http://localhost:8080/" for site "Development Web Site" application "/". Error description: Cannot create a file when that file already exists. (0x800700b7)
Registration completed
Unable to start iisexpress.

Cannot create a file when that file already exists.
For more information about the error, run iisexpress.exe with the tracing switch enabled (/trace:error).

I began to think that I was going to need to keep a spreadsheet with all of my paths and ports listed in it, when I realized that what I really needed was a common, generic batch file that would suit my needs for all of my development websites - with no customization at all.

Here is the batch file that I wrote, which I called "IISEXPRESS-START.cmd", and I will explain what it does after the code listing:

@echo off

pushd "%~dp0"

setlocal enabledelayedexpansion

if exist "wwwroot" (
  if exist "%ProgramFiles%\IIS Express\iisexpress.exe" (
    set /a RNDPORT=8000 + %random% %%1000
    "%ProgramFiles%\IIS Express\iisexpress.exe" /path:"%~dp0wwwroot" /port:!RNDPORT!
  )
)

popd

Here's what the respective lines in the batch file are doing:

  1. Push the folder where the batch file is being run on the stack; this is a cool little hack that also allows running the batch file in a folder on a network share and still have it work.
  2. Enable delayed expansion of variables; this ensures that variables would work inside of conditional code blocks.
  3. Check to see if there is a subfolder named "wwwroot" under the batch file's path; this is optional, but it helps things run smoothly. (I'll explain more about that below.)
  4. Check to make sure that IIS Express is installed in the correct place
  5. Configure a local variable with a random port number between 8000 and 8999; you can see that it uses modulus division to force the random port into the desired range.
  6. Start IIS Express by passing the path to the child "wwwroot" folder and the random port number.

One last piece of explanation: I almost always use a "wwwroot" folder under each parent folder for a website, with the goal of managing most of the parts of each website in one place. With that in mind, I tend to create folder hierarchies like the following example:

  • C:
    • Production
      • Website1
        • wwwroot
      • Website2
        • ftproot
        • wwwroot
      • Website3
        • wwwdata
        • wwwroot
    • Staging
      • Website1
        • wwwroot
      • Website2
        • ftproot
        • wwwroot
      • Website3
        • wwwdata
        • wwwroot

Using this structure, I can drop the batch file listed in this blog into any of those Website1, Website2, Website3 folders and it will "just work."


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Adjusting Pitch for MP3 Files with FFmpeg

I recently ran into a situation where I needed to adjust the pitch of an MP3 file for a song that I needed to learn. The problem was that song was recorded in a specific key, and I needed to play the song a half-step different. Of course, rehearsing in the original key and transposing on-the-fly is pretty trivial, but sometimes I prefer to learn a song in the key which I will be playing.

In the past I have always used a tool like Cakewalk Sonar to load the MP3 file, adjust the pitch, and then save out the adjusted audio. But I thought that was far too prosaic of an approach; I wanted a way to script the pitch change. This got me thinking about one of my favorite tools: FFmpeg.

I have mentioned FFmpeg in previous blogs, and it's one of my favorite tools; I use it almost every day for one purpose or other, and I have a large collection of batch files to automate various tasks. But unfortunately, I didn't have anything for adjusting audio pitch. That being said, I have done a lot with various FFmpeg audio and video filters, and after a little while of sifting through some of the various settings I came up with a way to easily change the pitch for an MP3 file. (And if I ever need to automate a whole directory of MP3 files, it would be simple to update this script with a loop.)

Here's the secret to the way this works - there are two audio filters that I am using:

  • asetrate - this filter adjusts the sample rate; altering the sample rate will stretch or shrink the audio, thereby changing the pitch and length of the audio.
  • atempo - this filter adjusts the tempo of the audio; altering the tempo will change the length of the audio, without changing the pitch.

So the trick is to use these two filters inversely; in other words:

  • If you increase the sample rate by 2, then you need to decrease the tempo by 2.
  • If you decrease the sample rate by 1.5, then you need to increase the tempo by 1.5.

With that in mind, I pulled out one of my favorite math constants: 2^(1/12), which is roughly 1.0594630943592952645618252949463. You might recall from some of my other blogs that this is the value by which every pitch in Equal Temperament is derived; in other words, that value is used to create every note in the chromatic scale which is used throughout the planet.

Taking that into account, I looked at the filter settings that were possible for use with FFmpeg:

  • If I assume that MP3 files are using a sample rate of 44.1khz, then I need to use values for the asetrate filter which raise or lower the sample rate by r*2^(n/12), where:
    • r is the sample rate.
    • n is the number of half steps to raise or lower.
  • The atempo can be values between 0.5 and 2.0, where:
    • 0.5 is half-tempo
    • 1.0 is the original tempo
    • 2.0 is double-tempo
    With that in mind, I used a similar formula to increase or decrease the tempo by 2^(n/12), where n is the number of half steps to raise or lower.

The math is a little weird, I'll admit - but it's pretty straight-forward. And here's the great part for you: I've already done the math, and I've written a batch file which defines a set of constants that can be used in batch files to script the raising or lowering the pitch of an MP3 file.

Here's the code for the batch file:

@echo off

set TMPFILE1=InputFile.mp3
set TMPFILE2=OutputFile.mp3

set RAISE_PITCH_01=asetrate=r=46722.3224612449211671764955071340,atempo=0.94387431268169349664191315666753
set RAISE_PITCH_02=asetrate=r=49500.5763304433484812188074908520,atempo=0.89089871814033930474022620559051
set RAISE_PITCH_03=asetrate=r=52444.0337716199990422417487017170,atempo=0.84089641525371454303112547623321
set RAISE_PITCH_04=asetrate=r=55562.5183003639065662339877809700,atempo=0.79370052598409973737585281963615
set RAISE_PITCH_05=asetrate=r=58866.4375688985154890396859602340,atempo=0.74915353843834074939964036601490
set RAISE_PITCH_06=asetrate=r=62366.8181006534916521544727376480,atempo=0.70710678118654752440084436210485
set RAISE_PITCH_07=asetrate=r=66075.3420902616540970482802825140,atempo=0.66741992708501718241541594059223
set RAISE_PITCH_08=asetrate=r=70004.3863917975968365502186919090,atempo=0.62996052494743658238360530363911
set RAISE_PITCH_09=asetrate=r=74167.0638253776226953452670037700,atempo=0.59460355750136053335874998528024
set RAISE_PITCH_10=asetrate=r=78577.2669399779266780879513330830,atempo=0.56123102415468649071676652483959
set RAISE_PITCH_11=asetrate=r=83249.7143785253664038167404180770,atempo=0.52973154717964763228091264747317
set RAISE_PITCH_12=asetrate=r=88200.0000000000000000000000000000,atempo=0.50000000000000000000000000000000

set LOWER_PITCH_01=asetrate=r=41624.8571892626832019083702090380,atempo=1.05946309435929526456182529494630
set LOWER_PITCH_02=asetrate=r=39288.6334699889633390439756665420,atempo=1.12246204830937298143353304967920
set LOWER_PITCH_03=asetrate=r=37083.5319126888113476726335018850,atempo=1.18920711500272106671749997056050
set LOWER_PITCH_04=asetrate=r=35002.1931958987984182751093459540,atempo=1.25992104989487316476721060727820
set LOWER_PITCH_05=asetrate=r=33037.6710451308270485241401412570,atempo=1.33483985417003436483083188118450
set LOWER_PITCH_06=asetrate=r=31183.4090503267458260772363688240,atempo=1.41421356237309504880168872420970
set LOWER_PITCH_07=asetrate=r=29433.2187844492577445198429801170,atempo=1.49830707687668149879928073202980
set LOWER_PITCH_08=asetrate=r=27781.2591501819532831169938904850,atempo=1.58740105196819947475170563927230
set LOWER_PITCH_09=asetrate=r=26222.0168858099995211208743508580,atempo=1.68179283050742908606225095246640
set LOWER_PITCH_10=asetrate=r=24750.2881652216742406094037454260,atempo=1.78179743628067860948045241118100
set LOWER_PITCH_11=asetrate=r=23361.1612306224605835882477535670,atempo=1.88774862536338699328382631333510
set LOWER_PITCH_12=asetrate=r=22050.0000000000000000000000000000,atempo=2.00000000000000000000000000000000

ffmpeg -y -i "%TMPFILE1%" -af "%RAISE_PITCH_01%" "%TMPFILE2%"

The only parts that you need to configure are:

  • TMPFILE1 - set this variable to the name of your original input MP3 file.
  • TMPFILE2 - set this variable to the name of your adjusted pitch output MP3 file.
  • Specify whether to raise or lower the pitch in the FFmpeg command by choosing one of the constants defined in the batch file; for example:
    • RAISE_PITCH_02 would raise the pitch of the original audio file by two half-steps (or one whole step).
    • LOWER_PITCH_05 would lower the pitch of the original audio file by five half-steps (or 2½ whole steps).

There are, of course, hundreds of other parameters which you can pass to FFmpeg in order to customize how FFmpeg processes the audio, but those are way out of scope for this blog.

With that in mind, that's it for now; have fun!

Fixing Underwater Videos with FFMPEG

I ran into an interesting predicament: I couldn't get the right color adjustment settings to work in my video editor to correct some underwater videos from a scuba diving trip. After much trial and error, I came up with an alternative method: I have been able to successfully edit underwater photos to restore their color, so I used FFMPEG to export all of the frames from the source video as individual images, then I used a script to automate my photo editor to batch process all of the images, then I used FFMPEG to reassemble the finished results into a new MP4 file.

The following video of a Goliath Triggerfish in Bora Bora shows a before and after of what that looks like. Overall, I think the results are promising, albeit via a weird and somewhat time-consuming hack.

Exporting Videos as Images with FFMPEG

Here is the basic syntax for automating FFMPEG to export the individual frames:

ffmpeg.exe -i "input.mp4" -r 60 -s hd1080 "C:\path\%6d.png"

Where the following items are defined:

-i "input.mp4" specifies the source MP4 file
-r 60 specifies the frame rate for the video at 60fps
-s hd1080 specifies 1920x1080 resolution (there are others)
"C:\path\%6d.png" specifies the directory for storing the images, and specifies PNG images with file names which are numerically sequenced with a width of 6 digits (e.g. 000000.png to 999999.png)

Combining Images as a Video with FFMPEG

Here is the basic syntax for automating FFMPEG to combine the individual frames back into an MP4 file:

ffmpeg.exe -framerate 60 -i "C:\path\%6d.png" -c:v libx264 -f mp4 -pix_fmt yuv420p "output.mp4"

Where the following items are defined:

-framerate 60 specifies the frame rate for the output video at 60fps (note that specifying a different framerate than you used for exporting could be used to alter the playback speed of the final video)
-i "C:\path\%6d.png" specifies the directory where the images are stored, and specifies PNG images with file names which are numerically sequenced with a width of 6 digits (e.g. 000000.png to 999999.png)
-c:v libx264 specifies the H.264 codec
-f mp4 specifies an MP4 file
-pix_fmt yuv420p specifies the pixel format, which could also specify "rgb24" instead of "yuv420p"
"output.mp4" specifies the final MP4 file

Creating an HTML Application to Convert Text Files to Audio Files

I'd like to take a brief departure from my normal collage of web-related and server-management examples and share a rather eclectic code sample.

Here's the scenario: I am presently working on another college degree, and I was recently taking a class which required a great deal of reading.  These assignments were all in digital form: I was using PDF or Kindle-based versions of the textbooks, and the remaining reading consisted of online articles. However, I am also an avid bicyclist, and the voluminous amount of reading was preventing me from going on some of my normal weekly rides. This gave me an idea: if I could convert the digital text to audio, I could bring my assignments with me on my longer rides and have my MP3 player read my assignments to me as I pedaled my way around the Arizona deserts.

I had experimented with Microsoft's built-in text-to-speech features some years ago, so I thought that this would be the perfect opportunity to revisit some of those APIs and write a full application to do the conversions for me. That being said, I could have written this application by using C# (which is my preferred language), but I decided to create an HTML Application for two reasons: 1) it is easier for me to share HTMLA applications in a blog, and 2) I keep this application in my OneDrive, so it's easier for me to modify the source code on systems where I don't have Visual Studio installed.

With that in mind, here is the resulting script which will convert a Text File to a Wave (*.WAV) File.

Using the HTML Application

As with most of my HTML Applications, the user interface is pretty simple to use; when you double click the HTA file, it will present you with the following user interface:

Clicking the Browse button will obviously allow you to browse to a text file, clicking the Close button will close the application, and clicking the Write File button will create a Wave file that is in the same path as the source text file. For example, if you have a text file at "C:\Text\Test.txt", the script will create a Wave file at "C:\Text\Test.txt.wav".

That being said, there are a few options which you can set:

  • Depending on your version of Windows and which languages you have installed, you will be presented with a list of available voices in the first drop-down menu; the following example is from a Windows 8 computer:
  • The second drop-down menu allows you to vary the playback speed; sometimes I prefer the playback speed to be slighter faster than normal:
  • The third drop-down menu allows you to alter the volume of the resulting Wave file:

Note: You can modify the script to alter the values that are used in the playback speed and volume drop-down menus, but the list of voices is obtained dynamically from your operating system.

Creating the HTML Application

To create this HTML Application, save the following HTMLA code as "Text to Wave File.hta" to your computer, and then double-click its icon to run the application. (Note that in a few places I added code comments which contain the MSDN URL for the APIs that I am using for this sample.)

<html>
<head>
<title>Text-to-Speech Writer</title>
<HTA:APPLICATION
  APPLICATIONNAME="Text-to-Speech Writer"
  ID="TextToSpeech"
  VERSION="1.0"
  BORDER="dialog"
  BORDERSTYLE="static"
  INNERBORDER="no"
  CAPTION="yes"
  SYSMENU="no"
  MAXIMIZEBUTTON="no"
  MINIMIZEBUTTON="no"
  SCROLL="no"
  SCROLLFLAT="yes"
  SINGLEINSTANCE="yes"
  CONTEXTMENU="no"
  SELECTION="no"/>
</head>

<script language="VBScript">

Option Explicit

Dim blnCancelBubble

' ----------------------------------------
' 
' OnLoad event handler for the application.
' 
' ----------------------------------------

Sub Window_OnLoad
  blnCancelBubble = False
  ' Set up the UI dimensions.
  Const intDialogWidth = 550
  Const intDialogHeight = 125
  ' Specify the window position and size.
  Self.resizeTo intDialogWidth,intDialogHeight
  Self.moveTo (Screen.AvailWidth - intDialogWidth) / 2,_
    (Screen.AvailHeight - intDialogHeight) / 2
  ' Load the list of text-to-speech voices into the drop-down menu.
  ' See the notes in WriteFile() for more information.
  Dim objSAPI, objVoice, objSelect, objOption
  Set objSAPI = CreateObject("SAPI.SpVoice")
  For Each objVoice In objSAPI.GetVoices("","")
    Set objSelect = Document.getElementById("optVoices")
    Set objOption = Document.createElement("option")
    objOption.text = objVoice.GetDescription() 
    objSelect.Add objOption
  Next
End Sub

' ----------------------------------------
' 
' Click handler for the Write button.
' 
' ----------------------------------------

Sub btnWrite_OnClick()
  ' Test for a file name.
  If Len(txtFile.Value) > 0 Then
    ' Test if we need to cancel bubbling of events.
    If blnCancelBubble = False Then
        ' Write the input file.
        Call WriteFile(txtFile.Value)
      End If
    End If
    ' Specify whether to bubble events.
    blnCancelBubble = IIf(blnCancelBubble=True,False,True)
End Sub

' ----------------------------------------
' 
' Change handler for the input box.
' 
' ----------------------------------------

Sub txtFile_OnChange()
  ' Enable the Write button.
  btnWrite.Disabled = False
  ' Enable event bubbling.
  blnCancelBubble = False
End Sub

' ----------------------------------------
' 
' Click handler for the Close button.
' 
' ----------------------------------------

Sub btnClose_OnClick()
  ' Test if we need to cancel bubbling of events.
  If blnCancelBubble = False Then
    ' Prompt the user to exit.
    If MsgBox("Are you sure you wish to exit?", _
      vbYesNo+vbDefaultButton2+vbQuestion+vbSystemModal, _
      TextToSpeech.applicationName)=vbYes Then
      ' Enable event bubbling.
      blnCancelBubble = True
      ' Close the application.
      Window.close
    End If
  End If
  ' Specify whether to bubble events.
     blnCancelBubble = IIf(blnCancelBubble=True,False,True)
End Sub

' ----------------------------------------
' 
' This is an ultra-lame workaround for the lack
' of a DoEvents() feature in HTA applications.
' 
' ----------------------------------------

Sub DoEvents()
  On Error Resume Next
  ' Create a shell object.
  Dim objShell : Set objShell = CreateObject("Wscript.Shell")
  ' Call out to the shell and essentially do nothing.
  objShell.Run "ver", 0, True
End Sub

' ----------------------------------------
' 
' This is an ultra-lame workaround for the lack
' of an IIf() function in vbscript applications.
' 
' ----------------------------------------

Function IIf(tx,ty,tz)
  If (tx) Then IIf = ty Else IIf = tz
End Function

' ----------------------------------------
' 
' Main text-to-speech function
' 
' ----------------------------------------

Sub WriteFile(strInputFileName)
  On Error Resume Next
  Dim objFSO
  Dim objFile
  Dim objSAPI
  Dim objFileStream
  Dim strOldTitle
  Dim strOutputFilename
  Const strProcessing = "Creating WAV file... "
  
  ' Define the audio format as 44.1kHz / 16-bit audio.
  ' See http://msdn.microsoft.com/en-us/library/ms720595.aspx
  Const SAFT44kHz16BitStereo = 35
  ' Allow text to be read as well as written.
  ' See http://msdn.microsoft.com/en-us/library/ms720858.aspx
  Const SSFMCreateForWrite = 3
  
  ' Define the output WAV filename.
  strOutputFilename = strInputFileName & ".wav"

  ' Create a file system object and open the input file.
  Set objFSO = CreateObject("Scripting.FileSystemObject")
  Set objFile = objFSO.OpenTextFile(strInputFileName, 1)
  
  ' Disable the form fields.
  optVoices.Disabled = True
  optRate.Disabled = True
  optVolume.Disabled = True
  txtFile.Disabled = True
  btnWrite.Disabled = True
  btnClose.Value = "Cancel"

  ' Test for an error.
  If Err.Number <> 0 Then
      MsgBox "Error: " & Err.Number & vbCrLf & Err.Description
  Else
    ' Store the original dialog title.
    strOldTitle = Document.title
    ' Display a status message.
    Document.title = strProcessing & Time()
    ' Pause briefly to let the screen refresh and capture events.
    Call DoEvents()
    ' Create a text-to-speech object.
    ' See http://msdn.microsoft.com/en-us/library/ms720149.aspx
    Set objSAPI = CreateObject("SAPI.SpVoice")
    ' Create a SAPI file stream object.
    ' See http://msdn.microsoft.com/en-us/library/ms722561.aspx
    Set objFileStream = CreateObject("SAPI.SpFileStream")
    ' Specify the stream format.
    ' See http://msdn.microsoft.com/en-us/library/ms720998.aspx
    objFileStream.Format.Type = SAFT44kHz16BitStereo
    ' Open the output file stream.
    objFileStream.Open strOutputFilename, SSFMCreateForWrite
    ' Specify the output file stream.
    ' See http://msdn.microsoft.com/en-us/library/ms723597.aspx
    Set objSAPI.AudioOutputStream = objFileStream
    
    ' Specify the speaking rate.
    ' See http://msdn.microsoft.com/en-us/library/ms723606.aspx
    objSAPI.Rate = optRate.Options(optRate.SelectedIndex).Value
    ' Specify the speaking volume.
    ' See http://msdn.microsoft.com/en-us/library/ms723615.aspx
    objSAPI.Volume = optVolume.Options(optVolume.SelectedIndex).Value
    ' Specify the voice to use.
    ' See http://msdn.microsoft.com/en-us/library/ms723601.aspx
    ' See http://msdn.microsoft.com/en-us/library/ms723614.aspx
    Set objSAPI.Voice = objSAPI.GetVoices("","").Item(optVoices.SelectedIndex)
    
    ' Loop through the lines in the input file.
    Do While Not objFile.AtEndOfStream
      ' Test if we need to cancel bubbling of events.
      If blnCancelBubble = True Then
        Exit Do
      Else
        ' Display a status message.
        Document.title = strProcessing & Time()
        ' Pause briefly to let the screen refresh and capture events.
        Call DoEvents()
        ' Speak one line from the input file.
        ' See http://msdn.microsoft.com/en-us/library/ms723609.aspx
        objSAPI.Speak objFile.ReadLine
      End If
    Loop
    ' Close the output file stream.
    objFileStream.Close
  End If

  ' Close the input file.
  objFile.Close

  ' Destroy all objects.
  Set objFileStream = Nothing
  Set objSAPI = Nothing
  Set objFile = Nothing
  Set objFSO = Nothing
  
  ' Reset the original dialog title.
  Document.title = strOldTitle

  ' Notify the user that the file has been written.
  MsgBox "Finished!", vbInformation, strOldTitle
  
  ' Re-enable the form fields.
  btnClose.Value = "Close"
  optVoices.Disabled = False
  optRate.Disabled = False
  optVolume.Disabled = False
  txtFile.Disabled = False
  btnWrite.Disabled = False

End Sub

</script>

<body bgcolor="white" id="HtmlBody">
<div id="FormControls">
  <table>
    <tr>
      <td align="left">
        <input type="file"
        style="width:250px;height:22px"
        name="txtFile"
        id="txtFile"
        onchange="txtFile_OnChange">
      </td>
      <td align="left">
        <input type="button"
        style="width:125px;height:22px"
        name="btnWrite"
        id="btnWrite"
        value="Write File"
        disabled
        onclick="btnWrite_OnClick">
      </td>
      <td align="right">
        <input type="button"
        style="width:125px;height:22px"
        name="btnClose"
        id="btnClose"
        value="Close"
        onclick="btnClose_OnClick">
      </td>
    </tr>
    <tr>
      <td align="left">
        <select name="optVoices"
          style="width:250px;height:22px">
        </select>
      </td>
      <td align="left">
        <select name="optRate"
          style="width:125px;height:22px">
          <option value="-2">Slowest</option>
          <option value="-1">Slower</option>
          <option value="0" selected>Normal Speed</option>
          <option value="1">Faster</option>
          <option value="2">Fastest</option>
        </select>
      </td>
      <td align="right">
        <select name="optVolume"
          style="width:125px;height:22px">
          <option value="25">25% Volume</option>
          <option value="50">50% Volume</option>
          <option value="75">75% Volume</option>
          <option value="100" selected>Full Volume</option>
        </select>
      </td>
    </tr>
  </table>
</div>
</body>
</html>

Note that I intentionally chose to have this HTML Application convert the text to audio one line at a time; this slows the down the conversion process, but it allows the conversion to be cancelled if necessary. (My original version of this script would convert an entire text file at one time; since there was no way to cancel the operation, the script appeared to hang when converting larger text files.)

Additional Notes

Once you have created a Wave (*.WAV) file, you can optionally convert it to an MP3 file for use in an MP3 player or mobile phone. (Most devices should playback Wave files, but MP3 files are considerably smaller and more portable.) There are a variety of Wave-to-MP3 converters out there, but I prefer to use the LAME encoder, which is an open-source code project that is available on SourceForge. Once you have the LAME encoder project compiled, (or you have located and downloaded a pre-compiled version), you can use LAME.EXE from a command prompt to convert your Wave files into MP3 files.

That being said, I prefer to automate as much as possible, so I have written a batch file which converts all of the Wave files in a directory to MP3 files and renames the source Wave files with a "*.old" filename extension:

@echo off

for /f "usebackq delims=|" %%a in (`dir /b *.wav`) do (
  for %%b in (^"%%a^") do (
    if not exist "%%~db%%~pb%%~nb.mp3" (
      lame.exe -b 128 -m j "%%a" "%%~nb.mp3"
    )
    if exist "%%~db%%~pb%%~nb.mp3" (
      move "%%a" "%%a.old"
    )
  )
)

Note that the above batch file was written for text-to-speech use, and as such it defines a bit rate of 128kbps, which would be pretty low for music files. If you want to repurpose this batch file for higher bitrates, modify the value of the "-b" parameter for the LAME.EXE command.

That wraps it up for today's post.


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

How to Merge a Folder of MP4 Files with FFmpeg (Revisted)

I ran into an interesting situation the other day: I had a bunch of H.264 MP4 files which I had created with Handbrake that I needed to combine, and I didn't want to use my normal video editor (Sony Vegas) to perform the merge. I'm a big fan of FFmpeg, so I figured that there was some way to automate the merge without having to use an editor.

I did some searching around the Internet, and I couldn't find anyone who was doing exactly what I was doing, so I wrote my own batch file that combines some tricks that I have used to automate FFmpeg in the past with some ideas that I found through some video hacking forums. Here is the resulting batch file, which will combine all of the MP4 files in a directory into a single MP4 file named "ffmpeg_merge.mp4", which can be renamed to something else:

@echo off

if exist ffmpeg_merge.mp4 del ffmpeg_merge.mp4
if exist ffmpeg_merge.tmp del ffmpeg_merge.tmp
if exist *.ts del *.ts

for /f "usebackq delims=|" %%a in (`dir /on /b *.mp4`) do (
ffmpeg.exe -i "%%a" -c copy -bsf h264_mp4toannexb -f mpegts "%%a.ts"
)

for /f "usebackq delims=|" %%a in (`dir /b *.ts`) do (
echo file %%a>>ffmpeg_merge.tmp
)

ffmpeg.exe -f concat -i ffmpeg_merge.tmp -c copy -bsf aac_adtstoasc ffmpeg_merge.mp4

if exist ffmpeg_merge.tmp del ffmpeg_merge.tmp
if exist *.ts del *.ts

The merging process in this batch file is performed in two steps:

  • First, all of the individual MP4 files are remuxed into individual transport streams
  • Second, all of the individual transport streams are remuxed into a merged MP4 file

Here are the URLs for the official documentation on each of the FFmpeg switches and parameters that I used:

By the way, I realize that there may be better ways to do this with FFmpeg, so I am open to suggestions. ;-]

FTP ETW Tracing and IIS 8 - Part 2

Shortly after I published my FTP ETW Tracing and IIS 8 blog post, I was using the batch file from that blog to troubleshoot an issue that I was having with a custom FTP provider. One of the columns which I display in my results is Clock-Time, which is obviously a sequential timestamp that is used to indicate the time and order in which the events occurred.

(Click the following image to view it full-size.)

At first glance the Clock-Time values might appear to be a range of useless numbers, but I use Clock-Time values quite often when I import the data from my ETW traces into something like Excel and I need to sort the data by the various columns.

That being said, apart from keeping the trace events in order, Clock-Time isn't a very user-friendly value. However, LogParser has some great built-in functions for crunching date/time values, so I decided to update the script to take advantage of some LogParser coolness and reformat the Clock-Time value into a human-readable Date/Time value.

My first order of business was to figure out how to decode the Clock-Time value; since Clock-Time increases for each event, it is obviously an offset from some constant, and after a bit of searching I found that the Clock-Time value is the offset in 100-nanosecond intervals since midnight on January 1, 1601. (Windows uses that value in a lot of places, not just ETW.) Once I had that information, it was pretty easy to come up with a LogParser formula to convert the Clock-Time value into the local time for my system, which is much easier to read.

With that in mind, here is the modified batch file:

@echo off

rem ======================================================================

rem Clean up old log files
for %%a in (ETL CSV) do if exist "%~n0.%%a" del "%~n0.%%a"

echo Starting the ETW session for full FTP tracing...
LogMan.exe start "%~n0" -p "IIS: Ftp Server" 255 5 -ets
echo.
echo Now reproduce your problem.
echo.
echo After you have reproduced your issue, hit any key to close the FTP
echo tracing session. Your trace events will be displayed automatically.
echo.
pause>nul

rem ======================================================================

echo.
echo Closing the ETW session for full FTP tracing...
LogMan.exe stop "%~n0" -ets

rem ======================================================================

echo.
echo Parsing the results - this may take a long time depending on the size of the trace...
if exist "%~n0.etl" (
   TraceRpt.exe "%~n0.etl" -o "%~n0.csv" -of CSV
   LogParser.exe "SELECT [Clock-Time], TO_LOCALTIME(ADD(TO_TIMESTAMP('1601-01-01 00:00:00', 'yyyy-MM-dd hh:mm:ss'), TO_TIMESTAMP(DIV([Clock-Time],10000000)))) AS [Date/Time], [Event Name], Type, [User Data] FROM '%~n0.csv'" -i:csv -e 2 -o:DATAGRID -rtp 20
)

When you run this new batch file, it will display an additional "Date/Time" column with a more-informative value in local time for the sever where you captured the trace.

(Click the following image to view it full-size.)

The new Date/Time column is considerably more practical, so I'll probably keep it in the batch file that I use when I am troubleshooting. You will also notice that I kept the original Clock-Time column; I chose to do so because I will undoubtedly continue to use that column for sorting when I import the data into something else, but you can safely remove that column if you would prefer to use only the new Date/Time value.

That wraps it up for today's post. :-)


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

FTP ETW Tracing and IIS 8

In the past I have written a couple of blogs about using the FTP service's Event Tracing for Windows (ETW) features to troubleshoot issues; see FTP and ETW Tracing and Troubleshooting Custom FTP Providers with ETW for details. Those blog posts contain batch files which use the built-in Windows LogMan utility to capture an ETW trace, and they use downloadable LogParser utility to parse the results into human-readable form. I use the batch files from those blogs quite often, and I tend to use them a lot when I am developing custom FTP providers which add new functionality to my FTP servers.

Unfortunately, sometime around the release of Windows 8 and Windows Server 2012 I discovered that the ETW format had changed, and the current version of LogParser (version 2.2) cannot read the new ETW files. When you try to use the batch files from my blog with IIS 8, you see the following errors:

Verifying that LogParser.exe is in the path...
Done.

Starting the ETW session for full FTP tracing...
The command completed successfully.

Now reproduce your problem.

After you have reproduced your issue, hit any key to close the FTP tracing session. Your trace events will be displayed automatically.

Closing the ETW session for full FTP tracing...
The command completed successfully.

Parsing the results - this may take a long time depending on the size of the trace...
Task aborted.
Cannot open <from-entity>: Trace file "C:\temp\ftp.etl" has been created on a OS version (6.3) that is not compatible with the current OS version


Statistics:
-----------
Elements processed: 0
Elements output: 0
Execution time: 0.06 seconds

I meant to research a workaround at the time, but one thing led to another and I simply forgot about doing so. But I needed to use ETW the other day when I was developing something, so that seemed like a good time to quit slacking and come up with an answer. :-)

With that in mind, I came up with a very easy workaround, which I will present here. Once again, this batch file has a requirement on LogParser being installed on your system, but for the sake of brevity I have removed the lines from this version of the batch file which check for LogParser. (You can copy those lines from my previous blog posts if you want that functionality restored.)

Here's the way that this workaround is implemented: instead of creating an ETW log and then parsing it directly with LogParser, this new batch file invokes the built-in Windows TraceRpt command to parse the ETW file and save the results as a CSV file, which is then read by LogParser to view the results in a datagrid like the batch files in my previous blogs:

@echo off

rem ======================================================================

rem Clean up old log files
for %%a in (ETL CSV) do if exist "%~n0.%%a" del "%~n0.%%a"

echo Starting the ETW session for full FTP tracing...
LogMan.exe start "%~n0" -p "IIS: Ftp Server" 255 5 -ets
echo.
echo Now reproduce your problem.
echo.
echo After you have reproduced your issue, hit any key to close the FTP
echo tracing session. Your trace events will be displayed automatically.
echo.
pause>nul

rem ======================================================================

echo.
echo Closing the ETW session for full FTP tracing...
LogMan.exe stop "%~n0" -ets

rem ======================================================================

echo.
echo Parsing the results - this may take a long time depending on the size of the trace...
if exist "%~n0.etl" (
   TraceRpt.exe "%~n0.etl" -o "%~n0.csv" -of CSV
   LogParser.exe "SELECT [Clock-Time], [Event Name], Type, [User Data] FROM '%~n0.csv'" -i:csv -e 2 -o:DATAGRID -rtp 20
)

Here's another great thing about this new batch file - it will also work down-level on Windows 7 and Windows Server 2008; so if you have been using my previous batch files with IIS 7 - you can simply replace your old batch file with this new version. You will see a few differences between the results from my old batch files and this new version, namely that I included a couple of extra columns that I like to use for troubleshooting.

(Click the following image to view it full-size.)

There is one last thing which I would like to mention in closing: I realize that it would be much easier on everyone if Microsoft simply released a new version of LogParser which works with the new ETW format, but unfortunately there are no plans at the moment to release a new version of LogParser. And trust me - I'm just as depressed about that fact as anyone else. :-(


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

Rapid PHP Deployment for IIS using a Batch File

Whenever I am delivering a presentation where I need to use PHP, I typically use a batch file that I wrote in order to rapidly deploy PHP on the system that I am using for my demos. The batch file usually takes less than a second to run, which always seems to amaze people in the audience. As a result, I usually have several people ask me for my batch file after each presentation, so I thought that it would make a good subject for today's blog.

I should mention that I have used this batch file in order to demonstrate PHP with IIS in a variety of scenarios, and one of my favorite demos is when I would borrow someone's laptop and plug in a flash drive where I had IIS Express pre-installed, and then I would run the batch file in this blog to deploy PHP. Next I would launch IIS Express, open a web browser on their system, and then browse to http://localhost/ in order to show that IIS Express was working correctly. Lastly I would write a simple PHP "Hello World" page to show that PHP was up-and-running on their system in a matter of seconds.

That being said, I have to point out that there is a very important prerequisite that you must have in order to follow the steps in the blog: you need to start with a known-good installation of PHP from one of your systems, and I'll explain what I mean by that.

My batch file expects to find a folder containing ready-to-run files for PHP in order to deploy PHP on a new system. I originally obtained my PHP files by using the Web Platform Installer (WebPI) to install PHP, and then I copied the files to my flash drive or some other repository. (Note that WebPI usually installs PHP in the "%ProgramFiles(x86)%\PHP" folder.) If you don't want to use WebPI, you can also download PHP from http://windows.php.net/, but you're on your own for configuration.

Once I have the files from a known-good installation of PHP, I create the following folder structure in the location where I will be storing the files that I use to deploy PHP on other systems:

  • <root folder>
    • SETUP_PHP.cmd (the batch file from this blog)
    • PHP (the folder containing the PHP files)
      • PHP.INI
      • PHP-CGI.EXE
      • etc. (all of the remaining PHP files and folders)

One thing to note is that the PHP.INI file you use may contain paths which refer to specific directories on the system from which you are copying your PHP files, so you need to make sure that those paths will exist on the system where you deploy PHP.

Here is an example: when I used WebPI to install PHP 5.5 on a system with IIS, it installed PHP into my "%ProgramFiles(x86)%\PHP\v5.5" folder. During the installation process, WebPI updated the PHP file to reflect any paths that need to be defined. At the time that I put together my notes for this blog, those updates mainly applied to the path where PHP expects to find it's extensions:

extension_dir="C:\Program Files (x86)\PHP\v5.5\ext\"

What this means is - if you want to deploy PHP to some other path on subsequent systems, you will need to update at least that line in the PHP.INI file that you are using to deploy PHP. In my particular case, I prefer to deploy PHP to the "%SystemDrive%\PHP" path, but it can be anywhere as long as you update everything accordingly.

The following batch file will deploy the PHP files in the "%SystemDrive%\PHP" folder on your system, and then it will update IIS with the necessary settings for this PHP deployment to work:

@echo off

REM Change to the installation folder
pushd "%~dp0"

REM Cheap test to see if IIS is installed
if exist "%SystemRoot%\System32\inetsrv" (
  REM Check for the PHP installation files in a subfolder
  if exist "%~dp0PHP" (
    REM Check for an existing installation of PHP
    if not exist "%SystemDrive%\PHP" (
      REM Create the folder for PHP
      md "%SystemDrive%\PHP"
      REM Deploy the PHP files
      xcopy /erhky "%~dp0PHP\*" "%SystemDrive%\PHP"
    )
    pushd "%SystemRoot%\System32\inetsrv"
    REM Configure the IIS settings for PHP
    appcmd.exe set config -section:system.webServer/fastCgi /+"[fullPath='%SystemDrive%\PHP\php-cgi.exe',monitorChangesTo='php.ini',activityTimeout='600',requestTimeout='600',instanceMaxRequests='10000']" /commit:apphost
    appcmd.exe set config -section:system.webServer/fastCgi /+"[fullPath='%SystemDrive%\PHP\php-cgi.exe',monitorChangesTo='php.ini',activityTimeout='600',requestTimeout='600',instanceMaxRequests='10000'].environmentVariables.[name='PHP_FCGI_MAX_REQUESTS',value='10000']" /commit:apphost
    appcmd.exe set config -section:system.webServer/fastCgi /+"[fullPath='%SystemDrive%\PHP\php-cgi.exe',monitorChangesTo='php.ini',activityTimeout='600',requestTimeout='600',instanceMaxRequests='10000'].environmentVariables.[name='PHPRC',value='%SystemDrive%\PHP']" /commit:apphost
    appcmd.exe set config -section:system.webServer/handlers /+"[name='PHP_via_FastCGI',path='*.php',verb='GET,HEAD,POST',modules='FastCgiModule',scriptProcessor='%SystemDrive%\PHP\php-cgi.exe',resourceType='Either']" /commit:apphost
    popd
  )
)
popd

Once you have all of that in place, it usually takes less than a second to deploy PHP, which is why so many people seem interested during my presentations.

Note that you can deploy PHP for IIS Express just as easily by updating the "%SystemRoot%\System32\inetsrv" paths in the batch file to "%ProgramFiles%\IIS Express" or "%ProgramFiles(x86)%\IIS Express" paths. You can also use this batch file as part of a deployment process for PHP within a web farm; in which case, you will need to pay attention to the paths inside your PHP.INI file which I mentioned earlier.


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/

SkyDrive is an Abysmal Failure in Windows 8.1

OK - I have to admit, I have used SkyDrive for several years now, and I have learned to become dependent on it because I like having several of my files easily accessible everywhere I go and on every device.

Apparently that was a big mistake on my part, because the SkyDrive team at Microsoft has slowly made SkyDrive a piece of crap. Having just set up my laptop with a brand-new installation of Windows 8.1 (which I installed from scratch), I can honestly say that SkyDrive in Windows 8.1 is now a complete failure as far as I am concerned. So unfortunately I'm probably going to have to switch to a third-party cloud storage application - and that sucks.

Before I discuss what's screwed up with SkyDrive in Windows 8.1, I should first mention that Microsoft used to make Windows Live Mesh, which was much better than SkyDrive. Mesh allowed you to choose any folder on your system and synchronize it across any machine that you specified. (In contrast, SkyDrive only synchronizes folders which are directly beneath the parent SkyDrive folder.) What's more, Mesh had a built-in remote desktop feature that was much like the built-in Windows Remote Desktop functionality - except that it actually worked. (If you've ever tried to manage a firewall and get the built-in Windows Remote Desktop functionality working over the Internet through your firewall and across a NAT, you know what I mean.) Unfortunately Microsoft's long-standing policy appears to be the following: if Microsoft has two competing technologies, choose the lesser of the two and ship that, and then deprecate the better technology. (At least that's what happened with SkyDrive and Mesh.)

Anyway - here are just a few of things things that are screwed up about SkyDrive in Windows 8.1:

In Windows 7, you had to manually choose to install the SkyDrive desktop functionality, so this was an opt-in feature. Of course, I installed SkyDrive, and I used it often. Unlike Windows Live Mesh, you had to drop files in the SkyDrive folder, which was really inconvenient. But that's also the way that DropBox works, so I'm sure that's what the engineers who were designing SkyDrive were trying to emulate.

In any event, after I installed SkyDrive on several of my systems, all of my SkyDrive-based files were physically stored on each of my local systems, and they were adequately synchronized across all machines where I installed SkyDrive. If I wanted to temporarily disable SkyDrive on any system, I could right-click on the SkyDrive System Tray icon and choose to close it.

However, once I installed Windows 8.1, everything changed. First of all SkyDrive is not optional - it's just there, and it appears to be always on. What's worse, my files weren't actually on my laptop anymore; they looked like they were locally stored, but they were more like ghost files which would actually download from the Internet whenever I tried to access a file. This was a pain in the butt for the system utilities which I was storing in my SkyDrive - most of them ceased to function because the EXE would download, but none of the supplemental DLL files would. As a direct result, all of my system utilities failed to run.

After some poking around I discovered that I could right-click on the SkyDrive folder and choose to make it available offline, which worked - albeit with hours of waiting for 25GB of files to download over Wi-Fi. But I need to point out that I had to go out of my way to make SkyDrive work the way that it used to; and more importantly, I had to discover on my own how to make something work the way that it always did in the past. This is known as a "Breaking Change," although I prefer to call that "Bad Design."

But today is when everything went from bad to worse. I needed to go to an appointment, so I brought my laptop with me because I thought that I would be able to do some work while I waited for my scheduled appointment time. I had a folder in my SkyDrive with some work-related files in it, so this seemed like something that should just work.

But it didn't work. In fact, it failed miserably.

What happened is this: I arrived at my appointment and booted my laptop, but when I opened my SkyDrive folder, everything was missing. Needless to say, I was more than a little alarmed. I opened Windows Explorer and navigated to the folder for my user profile, where I saw two folders that were both named "SkyDrive." Since Windows does not allow two folders with the same name in the same directory, I knew that this was a display anomaly which was probably caused by identical desktop.ini files in the two SkyDrive directories. I opened a command prompt and changed directories to my user profile folder, and the directory listing showed two folders: "SkyDrive" and SkyDrive (2)".

So I was correct in my assumption, and I verbally expressed my exasperation on the idiocy of this situation. ("What the heck...? Those stupid sons-of-biscuits...") I could immediately tell that Windows 8.1 had screwed something up, and my life was going to suck until I sorted it out.

I will spare you the details for everything that I tried to do, but it involved a lot of copying & renaming of files & folders - and after several hours of troubleshooting I still didn't have it resolved. But just to make things worse, while I was doing my troubleshooting I discovered that I suddenly had three folders under my user profile: "SkyDrive" and SkyDrive (2)," and "SkyDrive.old". I searched the Internet, and I found out that a lot of users have seen this problem.

A... lot... of... users...

There seemed to be two common consensuses: 1) this was clearly a bug in SkyDrive on Windows 8.1, and 2) SkyDrive now sucks for this reason.

One thing became clear to me: SkyDrive was going to continue to make my life miserable until I got it out of the way long enough for me to fix things. If you do some searching on the Internet, you can find ways to disable SkyDrive through Windows group policy, but I didn't want it permanently disabled - I just wanted it out of the way long enough to sort out the problem with multiple folders. Incidentally, logging out as my user account and logging in as the local administrator account did not make this easier since SkyDrive.exe runs at the system level.

Eventually I had to resort to backing up all of my multiple SkyDrive folders to an alternate location, and then running the following batch file while I manually cleaned up the multiple folders:

@echo off
:here
for %%a in (explorer.exe skydrive.exe) do (
   wmic process where name='%%a' call terminate
)
goto :here

Note that I had to put these process termination statements in a loop because Windows would keep restarting both executables, thereby thwarting any repairs that I had managed to start.

Yes, this is a lame and prosaic approach to solving this problem, but releasing a major breaking change to a service upon which you hope everyone will depend is pretty darn lame, too. And making the new service so heinously awful that it's barely usable is unforgivably lame.

Eventually I got everything sorted out, and I would love to be able to write something definitive like, "You need to do X and Y and your system will be better." But truth-be-told, I spent so many hours trying so many things that I cannot be certain which specific steps resolved the issue. And I'm not about to attempt setting up a repro environment to test which steps to take. Sorry about that - but I simply don't want to mess with things now that I have SkyDrive working again.

Custom Post-Build Events for Compiling FTP Providers

I've written a lot of walkthroughs and blog posts about creating custom FTP providers over the past several years, and I usually include instructions like the following example for adding a custom post-build event that will automatically register your extensibility provider in the Global Assembly Cache (GAC) on your development computer:

  • Click Project, and then click the menu item your project's properties.
  • Click the Build Events tab.
  • Enter the following in the Post-build event command line dialog box:
    net stop ftpsvc
    call "%VS100COMNTOOLS%\vsvars32.bat">nul
    gacutil.exe /if "$(TargetPath)"
    net start ftpsvc

And I usually include instructions like the following example for determining the assembly information for your extensibility provider:

  • In Windows Explorer, open your "C:\Windows\assembly" path, where C: is your operating system drive.
  • Locate the FtpXmlAuthorization assembly.
  • Right-click the assembly, and then click Properties.
  • Copy the Culture value; for example: Neutral.
  • Copy the Version number; for example: 1.0.0.0.
  • Copy the Public Key Token value; for example: 426f62526f636b73.
  • Click Cancel.

Over time I have changed the custom post-build event that I use when I am creating custom FTP providers, and my changes make it easier to register custom FTP providers. With that in mind, I thought that my changes would make a good blog subject.

First of all, if you take a look at my How to Use Managed Code (C#) to Create a Simple FTP Authentication Provider walkthrough, you will see that I include instructions like my earlier examples to create a custom post-build event and retrieve the assembly information for your extensibility provider.

That being said, instead of using the custom post-build event in that walkthrough, I have started using the following custom post-build event:

net stop ftpsvc
call "$(DevEnvDir)..\Tools\vsvars32.bat"
gacutil.exe /uf "$(TargetName)"
gacutil.exe /if "$(TargetPath)"
gacutil.exe /l "$(TargetName)"
net start ftpsvc

This script should resemble the following example when entered into Visual Studio:

This updated script performs the following actions:

  1. Stops the FTP service (this will allow any copies of your DLL to unload)
  2. Loads the Visual Studio environment variables (this will add gacutil.exe to the path)
  3. Calls gacutil.exe to forcibly unregister any previous version of your FTP provider
  4. Calls gacutil.exe to forcibly register the newly-compiled version of your FTP provider
  5. Calls gacutil.exe to list the GAC information for your FTP provider (this will be used to register your DLL with IIS)
  6. Starts the FTP service

Let's say that you created a simple FTP authentication provider which contained code like the following example:

using System;
using System.Text;
using Microsoft.Web.FtpServer;

public class FtpTestProvider :
    BaseProvider,
    IFtpAuthenticationProvider
{
    private string _username = "test";
    private string _password = "password";
    
    public bool AuthenticateUser(
        string sessionId,
        string siteName,
        string userName,
        string userPassword,
        out string canonicalUserName)
    {
        canonicalUserName = userName;
        if (((userName.Equals(_username,
            StringComparison.OrdinalIgnoreCase)) == true) &&
            userPassword == _password)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

When you compile your provider in Visual Studio, the output window should show the results of the custom post-build event:

When you examine the output information in detail, the highlighted area in the example below should be of particular interest, because it contains the assembly information for your extensibility provider:

------ Rebuild All started: Project: FtpTestProvider, Configuration: Debug Any CPU ------
FtpTestProvider -> c:\users\foobar\documents\visual studio 2012\Projects\FtpTestProvider\bin\Debug\FtpTestProvider.dll
The Microsoft FTP Service service is stopping..
The Microsoft FTP Service service was stopped successfully.

Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.17929
Copyright (c) Microsoft Corporation. All rights reserved.

Assembly successfully added to the cache
Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.17929
Copyright (c) Microsoft Corporation. All rights reserved.

The Global Assembly Cache contains the following assemblies:
FtpTestProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=eb763c2ec0efff75, processorArchitecture=MSIL

Number of items = 1
The Microsoft FTP Service service is starting.
The Microsoft FTP Service service was started successfully.

========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

Once you have that information, you simply need to reformat it as "FtpTestProvider, FtpTestProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=eb763c2ec0efff75" in order to enter it into the FTP Custom Authentication Providers dialog box in the IIS Manager, or by following the steps in my FTP Walkthroughs or my Adding Custom FTP Providers with the IIS Configuration Editor blogs.

That wraps it up for today's post. As always, let me know if you have any questions. ;-]


Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/