# Microsoft Bob

Just a short, simple blog for Bob to share some tips and tricks.

Be sure to check out my non-technical blog at www.bobsbasement.net.

## IIS 6.0 WebDAV and Compound Document Format Files Revisited with Workarounds

A few years ago I wrote the following blog, wherein I described how the WebDAV functionality in IIS 6.0 worked with files that are Compound Document format:

IIS 6.0 WebDAV and Compound Document Format Files

As I explained in that blog post, WebDAV needs somewhere to store "properties" for files that are uploaded to the server, and WebDAV uses the compound document format to accomplish this according to the following implementation logic:

• If the file is already in the compound document file format, IIS simply adds the WebDAV properties to the existing file. This data will not be used by the application that created the file - it will only be used by WebDAV. However, the file size will increase because WebDAV properties are added to the compound document.
• For other files, WebDAV stores a compound document in an NTFS alternate data stream that is attached to the file. You will never see this additional data from any directory listing, and the file size doesn't change because it's in an alternate data stream.

I recently had a customer contact me in order to ask if there was a way to disable this functionality since he didn't want his files modified in order to store the WebDAV properties. Unfortunately there is no built-in option for IIS that will disable this functionality, but there are a few workarounds.

#### Workaround #1 - Change the File Type

First and foremost - you can change your file type to something other than the compound document format. For example, if you are uploading files that were created in Microsoft Office, if you can upload your files in the newer Office Open XML formats, then you will not run into this problem. By way of explanation, older Microsoft Office files are in compound document format, whereas files that are that are created with Microsoft Office 2010 and later are in a zipped, XML-based file format. These files will have extensions like *.DOCX for Microsoft Word documents, *.XLSX for Microsoft Excel spreadsheets, and *.PPTX for Microsoft PowerPoint presentations.

#### Workaround #2 - Wrap Compound Document Files in a Separate File Type

If you are using a file that must be in compound document format, like a setup package in Microsoft Installer (*.MSI) format, you can upload the file in a *.ZIP file, or you can wrap the setup package inside a self-extracting executable by using a technology like Microsoft's IExpress Wizard (which ships as a built-in utility with most versions of Windows).

#### Workaround #3 - Block WebDAV Properties

If you absolutely cannot change your document from compound document format, I have a completely unsupported workaround that I can suggest. Since the problem arises when properties are added to a file, you can find a way to intercept the WebDAV commands that try to set properties. The actual HTTP verb that is used is PROPPATCH, so if you can find a way to keep this command from being used, then you can prevent files from being modified. Unfortunately you cannot simply suppress PROPPATCH commands by using a security tool like Microsoft's UrlScan to block the command, because this will cause many WebDAV clients to fail.

Instead, what I did as a workaround was to write an example ISAPI filter for IIS 6.0 that intercepts incoming PROPPATCH commands and always sends a successful (e.g. "200 OK") response to the WebDAV client, but in reality the filter does nothing with the properties and ends the request processing. This tricks a WebDAV client into thinking that it succeeded, and it prevents your files in compound document format from being modified. However, this also means that no WebDAV properties will ever be stored with your files; but if that's acceptable to you, (and it usually should be), then you can use this workaround.

With that in mind, here's the C++ code for my example ISAPI filter, and please remember that this is a completely unsupported workaround that is intended for use only when you cannot repackage your files to use something other than the compound document format.

#define _WIN32_WINNT 0x0400

#include <windows.h>
#include <httpfilt.h>

#define STRSAFE_LIB
#include <strsafe.h>

#define BUFFER_SIZE 2048

const char xmlpart1[] = "<?xml version=\"1.0\"?>"
"<a:multistatus xmlns:a=\"DAV:\">"
"<a:response>"
"<a:href>";

const char xmlpart2[] = "</a:href>"
"<a:propstat>"
"<a:status>HTTP/1.1 200 OK</a:status>"
"</a:propstat>"
"</a:response>"
"</a:multistatus>";

BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer)
{
HRESULT hr = S_OK;
// Set the filter's version.
pVer->dwFilterVersion = HTTP_FILTER_REVISION;
// Set the filter's description.
hr = StringCchCopyEx(
pVer->lpszFilterDesc,256,"PROPPATCH",
NULL,NULL,STRSAFE_IGNORE_NULLS);
if (FAILED(hr)) return FALSE;
// Set the filter's flags.
return TRUE;
}

DWORD WINAPI HttpFilterProc(
PHTTP_FILTER_CONTEXT pfc,
{
{
HRESULT hr = S_OK;

bool fSecure = false;

char szServerName[BUFFER_SIZE] = "";
char szSecure[2] = "";
char szResponseXML[BUFFER_SIZE] = "";
char szResponseURL[BUFFER_SIZE] = "";
char szRequestURL[BUFFER_SIZE] = "";
char szMethod[BUFFER_SIZE] = "";

DWORD dwBuffSize = 0;

// Get the method of the request
dwBuffSize = BUFFER_SIZE-1;
// Exit with an error status if a failure occured.
if (!pfc->GetServerVariable(
pfc, "HTTP_METHOD", szMethod, &dwBuffSize))
return SF_STATUS_REQ_ERROR;

if (strcmp(szMethod, "PROPPATCH") == 0)
{
// Send the HTTP status to the client.
if (!pfc->ServerSupportFunction(
return SF_STATUS_REQ_ERROR;

// Get the URL of the request.
dwBuffSize = BUFFER_SIZE-1;
if (!pfc->GetServerVariable(
pfc, "URL", szRequestURL, &dwBuffSize))
return SF_STATUS_REQ_ERROR;

// Determine if request was sent over secure port.
dwBuffSize = 2;
if (!pfc->GetServerVariable(
pfc, "SERVER_PORT_SECURE", szSecure, &dwBuffSize))
return SF_STATUS_REQ_ERROR;
fSecure = (szSecure[0] == '1');

// Get the server name.
dwBuffSize = BUFFER_SIZE-1;
if (!pfc->GetServerVariable(
pfc, "SERVER_NAME", szServerName, &dwBuffSize))
return SF_STATUS_REQ_ERROR;

// Set the response URL.
hr = StringCchPrintf(
szResponseURL,BUFFER_SIZE-1, "http%s://%s/%s",
(fSecure ? "s" : ""), szServerName, &szRequestURL[1]);
// Exit with an error status if a failure occurs.
if (FAILED(hr)) return SF_STATUS_REQ_ERROR;

// Set the response body.
hr = StringCchPrintf(
szResponseXML,BUFFER_SIZE-1, "%s%s%s",
xmlpart1, szResponseURL, xmlpart2);
// Exit with an error status if a failure occurs.
if (FAILED(hr)) return SF_STATUS_REQ_ERROR;

// Write the response body to the client.
dwBuffSize = strlen(szResponseXML);
if (!pfc->WriteClient(
pfc, szResponseXML, &dwBuffSize, 0))
return SF_STATUS_REQ_ERROR;

// Flag the request as completed.
return SF_STATUS_REQ_FINISHED;
}
}

}

I hope this helps. ;-]

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

Posted: Apr 24 2013, 11:47 by Bob | Comments (0)
• Currently 0/5 Stars.
• 1
• 2
• 3
• 4
• 5
Filed under: IIS | WebDAV | IIS 6
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

## Adding Custom FTP Providers with the IIS Configuration Editor - Part 1

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 for adding these custom providers to IIS. When you create a custom FTP authentication provider, IIS has a user interface for adding that provider to FTP. But if you are adding a custom home directory or logging provider, there is no dedicated user interface for adding those types of FTP providers. In addition, if you create a custom FTP provider that requires settings that are stored in your IIS configuration, there is no user interface to add or manage those settings.

With this in mind, I include instructions in my blogs and walkthroughs that describe how to add those type of providers by using AppCmd.exe from a command line. For example, if you take a look at my How to Use Managed Code (C#) to Create an FTP Authentication and Authorization Provider using an XML Database walkthrough, I include the following instructions:

1. Determine the assembly information for the 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.
2. Using the information from the previous steps, add the extensibility provider to the global list of FTP providers and configure the options for the provider:
• At the moment there is no user interface that enables you to add properties for custom authentication or authorization modules, so you will have to use the following command line:

cd %SystemRoot%\System32\Inetsrv

appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpXmlAuthorization',type='FtpXmlAuthorization,FtpXmlAuthorization,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost

appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpXmlAuthorization']" /commit:apphost

appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpXmlAuthorization'].[key='xmlFileName',value='C:\Inetpub\XmlSample\Users.xml']" /commit:apphost
• Note: The file path that you specify in the xmlFileName attribute must match the path where you saved the "Users.xml" file on your computer in the earlier in this walkthrough.

This example adds a custom FTP provider, and then it adds a custom setting for that provider that is stored in your IIS configuration settings.

That being said, there is actually a way to add custom FTP providers with settings like the ones that I have just described through the IIS interface by using the IIS Configuration Editor. This feature was first available through the IIS Administration Pack for IIS 7.0, and is built-in for IIS 7.5 and IIS 8.0.

Before I continue, if would probably be prudent to take a look at the settings that we are trying to add, because these settings might help you to understand the rest of steps in this blog. Here is an example from my applicationhost.config file for three custom FTP authentication providers; the first two providers are installed with the FTP service, and the third provider is a custom provider that I created with a single provider-specific configuration setting:

<system.ftpServer>
<providerDefinitions>
<add name="FtpXmlAuthorization" type="FtpXmlAuthorization, FtpXmlAuthorization, version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73" />
<activation>
<providerData name="FtpXmlAuthorization">
</providerData>
</activation>
</providerDefinitions>
</system.ftpServer>

With that in mind, in part 1 of this blog series, I will show you how to use the IIS Configuration Editor to add a custom FTP provider with provider-specific configuration settings.

Step 1 - Open the IIS Manager and click on the Configuration Editor at feature the server level:

Step 2 - Click the Section drop-down menu, expand the the system.ftpServer collection, and then highlight the providerDefinitions node:

Step 3 - A default installation IIS with the FTP service should show a Count of 2 providers in the Collection row, and no settings in the activation row:

Step 4 - If you click on the Collection row, an ellipsis [...] will appear, and when you click that, IIS will display the Collection Editor dialog for FTP providers. By default you should see just the two built-in providers for the IisManagerAuth and AspNetAuth providers:

Step 5 - When you click Add in the Actions pane, you can enter the registration information for your provider. At a minimum you must provide a name for your provider, but you will need to enter either the clsid for a COM-based provider or the type for a managed-code provider:

Step 6 - When you close the Collection Editor dialog, the Count of providers in the Collection should now reflect the provider that we just added; click Apply in the Actions pane to save the changes:

Step 7 - If you click on the activation row, an ellipsis [...] will appear, and when you click that, IIS will display the Collection Editor dialog for provider data; this is where you will enter provider-specific settings. When you click Add in the Actions pane, you must specify the name for your provider's settings, and this name must match the exact name that you provided in Step 5 earlier:

Step 8 - If you click on the Collection row, an ellipsis [...] will appear, and when you click that, IIS will display the Collection Editor dialog for the activation data for an FTP provider. At a minimum you must provide a key for your provider, which will depend on the settings that your provider expects to retrieve from your configuration settings. (For example, in the XML file that I provided earlier, my FtpXmlAuthorization provider expects to retrieve the path to an XML that contains a list of users, roles, and authorization rules.) You also need to enter the either the value or encryptedValue for your provider; although you can specify either setting, should generally specify the value when the settings are not sensitive in nature, and specify the encryptedValue for settings like usernames and passwords:

Step 9 - When you close the Collection Editor dialog for the activation data, the Count of key/value pairs in the Collection should now reflect the value that we just added:

Step 10 - When you close the Collection Editor dialog for the provider data, the Count of provider data settings in the activation row should now reflect the custom settings that we just added; click Apply in the Actions pane to save the changes:

That's all that there is to adding a custom FTP provider with provider-specific settings; I admit that it might seem like a lot of steps until you get the hang of it.

In the next blog for this series, I will show you how to add custom providers to FTP sites by using the IIS Configuration Editor.

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

Posted: Mar 31 2013, 16:03 by Bob | Comments (0)
• Currently 0/5 Stars.
• 1
• 2
• 3
• 4
• 5
Filed under: Extensibility | FTP | IIS
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

## Automating the Creation of FTP User Isolation Folders

A customer asked me a question a little while ago that provided me the opportunity to recycle some code that I had written many years ago. In so doing, I also made a bunch of updates to the code to make it considerably more useful, and I thought that it would make a great blog.

Here's the scenario: a customer had hundreds of user accounts created, and he wanted to use the FTP service's User Isolation features to restrict each user to a specific folder on his FTP site. Since it would take a long time to manually create a folder for each user account, the customer wanted to know if there was a way to automate the process. As it turns out, I had posted a very simple script in the IIS.net forums several years ago that did something like what he wanted; and that script was based off an earlier script that I had written for someone else back in the IIS 6.0 days.

One quick reminder - FTP User Isolation uses a specific set of folders for user accounts, which are listed in the table below.

User Account TypesHome Directory Syntax
Anonymous users %FtpRoot%\LocalUser\Public
Local Windows user accounts

(Requires Basic authentication.)

Windows domain accounts

(Requires Basic authentication.)

Note: %FtpRoot% is the root directory for your FTP site: for example, C:\Inetpub\Ftproot.

That being said, I'm a big believer in recycling code, so I found the last version of that script that I gave to someone and I made a bunch of changes to it so it would be more useful for the customer. What that in mind, here's the resulting script, and I'll explain a little more about what it does after the code sample.

Option Explicit

' Define the root path for the user isolation folders.
' This should be the root directory for your FTP site.
Dim strRootPath : strRootPath = "C:\Inetpub\wwwroot\"

' Define the name of the domain or the computer to use.
' Leave this blank for the local computer.
Dim strComputerOrDomain : strComputerOrDomain = ""

' Define the remaining script variables.
Dim objFSO, objCollection, objUser, objNetwork, strContainerName

' Create a network object; used to query the computer name.
Set objNetwork = WScript.CreateObject("WScript.Network")

' Create a file system object; used to creat folders.
Set objFSO = CreateObject("Scripting.FileSystemObject")

' Test if the computer name is null.
If Len(strComputerOrDomain)=0 Or strComputerOrDomain="." Then
' If so, define the local computer name as the account repository.
strComputerOrDomain = objNetwork.ComputerName
End If

' Verify that the root path exists.
If objFSO.FolderExists(strRootPath) Then

' Test if the script is using local users.
If StrComp(strComputerOrDomain,objNetwork.ComputerName,vbTextCompare)=0 Then
' If so, define the local users container path.
strContainerName = "LocalUser"
' And define the users collection as local.
Set objCollection = GetObject("WinNT://.")
Else
' Otherwise, use the source name as the path.
strContainerName = strComputerOrDomain
' And define the users collection as remote.
Set objCollection = GetObject("WinNT://" & strComputerOrDomain & "")
End If

' Append trailing backslash if necessary.
If Right(strRootPath,1)<>"\" Then strRootPath = strRootPath & "\"
' Define the adjusted root path for the container folder.
strRootPath = strRootPath & strContainerName & "\"

' Test if the container folder already exists.
If objFSO.FolderExists(strRootPath)=False Then
' Create the container folder if necessary.
objFSO.CreateFolder(strRootPath)
End If

' Specify the collection filter for user objects only.
objCollection.Filter = Array("user")

' Loop through the users collection.
For Each objUser In objCollection
' Test if the user's account is enabled.
If objUser.AccountDisabled = False Then
' Test if the user's folder already exists.
If objFSO.FolderExists(strRootPath & "\" & objUser.Name)=False Then
' Create the user's folder if necessary.
objFSO.CreateFolder(strRootPath & "\" & objUser.Name)
End If
End If
Next

End If

I documented this script in great detail, so it should be self-explanatory for the most part. But just to be on the safe side, here's an explanation of what this script is doing when you run it on your FTP server:

• Defines two user-updatable variables:
• strRootPath - which specifies the physical path to the root of your FTP site.
• strComputerOrDomain - which specifies the computer name or the domain name where your user accounts are located. (Note: You can leave this blank if you are using local user accounts on your FTP server.)
• Creates a few helper objects and determines the local computer name if necessary.
• Checks to see if the physical path to the root of your FTP site actually exists before continuing.
• Creates a connection to the user account store (local or domain).
• Determines the container folder name that be the parent directory of user account folders, and creates it if necessary. (See my earlier note about the folder names.)
• Defines a filter for user objects in the specifies account repository. (This removes computer accounts and such from the operation.)
• Loops through the collection of user accounts, checks each account to see if it is enabled, and creates a folder for each user account if it does not already exist.

That's all for now. ;-]

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Mar 28 2013, 16:29 by Bob | Comments (0)
• Currently 0/5 Stars.
• 1
• 2
• 3
• 4
• 5
Filed under: IIS | Scripting | FTP
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

## Advanced Log Parser Part 7 - Creating a Generic Input Format Plug-In

In Part 6 of this series, I showed how to create a very basic COM-based input format provider for Log Parser. I wrote that blog post as a follow-up to an earlier blog post where I had written a more complex COM-based input format provider for Log Parser that worked with FTP RSCA events. My original blog post had resulted in several requests for me to write some easier examples about how to get started writing COM-based input format providers for Log Parser, and those appeals led me to write my last blog post:

Advanced Log Parser Part 6 - Creating a Simple Custom Input Format Plug-In

The example in that blog post simply returns static data, which was the easiest example that I could demonstrate.

For this follow-up blog post, I will illustrate how to create a simple COM-based input format plug-in for Log Parser that you can use as a generic provider for consuming data in text-based log files. Please bear in mind that this is just an example to help developers get started writing their own COM-based input format providers; you might be able to accomplish some of what I will demonstrate in this blog post by using the built-in Log Parser functionality. That being said, this still seems like the best example to help developers get started because consuming data in text-based log files was the most-often-requested example that I received.

### In Review: Creating COM-based plug-ins for Log Parser

In my earlier blog posts, I mentioned that a COM plug-in has to support several public methods. You can look at those blog posts when you get the chance, but it is a worthwhile endeavor for me to copy the following information from those blog posts since it is essential to understanding how the code sample in this blog post is supposed to work.

Method NameDescription
OpenInput Opens your data source and sets up any initial environment settings.
GetFieldCount Returns the number of fields that your plug-in will provide.
GetFieldName Returns the name of a specified field.
GetFieldType Returns the datatype of a specified field.
GetValue Returns the value of a specified field.
CloseInput Closes your data source and cleans up any environment settings.

Once you have created and registered a COM-based input format plug-in, you call it from Log Parser by using something like the following syntax:

logparser.exe "SELECT * FROM FOO" -i:COM -iProgID:BAR

In the preceding example, FOO is a data source that makes sense to your plug-in, and BAR is the COM class name for your plug-in.

### Creating a Generic COM plug-in for Log Parser

As I have done in my previous two blog posts about creating COM-based input format plug-ins, I'm going to demonstrate how to create a COM component by using a scriptlet since no compilation is required. This generic plug-in will parse any text-based log files where records are delimited by CRLF sequences and fields/columns are delimited by a separator that is defined as a constant in the code sample.

To create the sample COM plug-in, copy the following code into a text file, and save that file as "Generic.LogParser.Scriptlet.sct" to your computer. (Note: The *.SCT file extension tells Windows that this is a scriptlet file.)

<SCRIPTLET>
<registration
Description="Simple Log Parser Scriptlet"
Progid="Generic.LogParser.Scriptlet"
Classid="{4e616d65-6f6e-6d65-6973-526f62657274}"
Version="1.00"
Remotable="False" />
<comment>
EXAMPLE: logparser "SELECT * FROM 'C:\foo\bar.log'" -i:COM -iProgID:Generic.LogParser.Scriptlet
</comment>
<implements id="Automation" type="Automation">
<method name="OpenInput">
<parameter name="strFileName"/>
</method>
<method name="GetFieldCount" />
<method name="GetFieldName">
<parameter name="intFieldIndex"/>
</method>
<method name="GetFieldType">
<parameter name="intFieldIndex"/>
</method>
<method name="GetValue">
<parameter name="intFieldIndex"/>
</method>
<method name="CloseInput">
<parameter name="blnAbort"/>
</method>
</implements>
<SCRIPT LANGUAGE="VBScript">

Option Explicit

' Define the column separator in the log file.
Const strSeparator = "|"

' Define whether the first row contains column names.

' Define the field type constants.
Const TYPE_INTEGER   = 1
Const TYPE_REAL      = 2
Const TYPE_STRING    = 3
Const TYPE_TIMESTAMP = 4
Const TYPE_NULL      = 5

' Declare variables.
Dim objFSO, objFile, blnFileOpen
Dim arrFieldNames, arrFieldTypes
Dim arrCurrentRecord

' Indicate that no file has been opened.
blnFileOpen = False

' --------------------------------------------------------------------------------
' Open the input session.
' --------------------------------------------------------------------------------

Public Function OpenInput(strFileName)
Dim tmpCount
' Test for a file name.
If Len(strFileName)=0 Then
' Return a status that the parameter is incorrect.
OpenInput = 87
blnFileOpen = False
Else
' Test for single-quotes.
If Left(strFileName,1)="'" And Right(strFileName,1)="'" Then
' Strip the single-quotes from the file name.
strFileName = Mid(strFileName,2,Len(strFileName)-2)
End If
' Open the file system object.
Set objFSO = CreateObject("Scripting.Filesystemobject")
' Verify that the specified file exists.
If objFSO.FileExists(strFileName) Then
' Open the specified file.
Set objFile = objFSO.OpenTextFile(strFileName,1,False)
' Set a flag to indicate that the specified file is open.
blnFileOpen = true
' Retrieve an initial record.
' Redimension the array of field names.
ReDim arrFieldNames(UBound(arrCurrentRecord))
' Loop through the record fields.
For tmpCount = 0 To (UBound(arrFieldNames))
' Test for a header row.
arrFieldNames(tmpCount) = arrCurrentRecord(tmpCount)
Else
arrFieldNames(tmpCount) = "Field" & (tmpCount+1)
End If
Next
' Test for a header row.
' Retrieve a second record.
End If
' Redimension the array of field types.
ReDim arrFieldTypes(UBound(arrCurrentRecord))
' Loop through the record fields.
For tmpCount = 0 To (UBound(arrFieldTypes))
' Test if the current field contains a date.
If IsDate(arrCurrentRecord(tmpCount)) Then
' Specify the field type as a timestamp.
arrFieldTypes(tmpCount) = TYPE_TIMESTAMP
' Test if the current field contains a number.
ElseIf IsNumeric(arrCurrentRecord(tmpCount)) Then
' Test if the current field contains a decimal.
If InStr(arrCurrentRecord(tmpCount),".") Then
' Specify the field type as a real number.
arrFieldTypes(tmpCount) = TYPE_REAL
Else
' Specify the field type as an integer.
arrFieldTypes(tmpCount) = TYPE_INTEGER
End If
' Test if the current field is null.
ElseIf IsNull(arrCurrentRecord(tmpCount)) Then
' Specify the field type as NULL.
arrFieldTypes(tmpCount) = TYPE_NULL
' Test if the current field is empty.
ElseIf IsEmpty(arrCurrentRecord(tmpCount)) Then
' Specify the field type as NULL.
arrFieldTypes(tmpCount) = TYPE_NULL
' Otherwise, assume it's a string.
Else
' Specify the field type as a string.
arrFieldTypes(tmpCount) = TYPE_STRING
End If
Next
' Temporarily close the log file.
objFile.Close
' Re-open the specified file.
Set objFile = objFSO.OpenTextFile(strFileName,1,False)
' Test for a header row.
' Skip the first row.
objFile.SkipLine
End If
' Return success status.
OpenInput = 0
Else
OpenInput = 2
End If
End If
End Function

' --------------------------------------------------------------------------------
' Close the input session.
' --------------------------------------------------------------------------------

Public Function CloseInput(blnAbort)
' Free the objects.
Set objFile = Nothing
Set objFSO = Nothing
' Set a flag to indicate that the specified file is closed.
blnFileOpen = False
End Function

' --------------------------------------------------------------------------------
' Return the count of fields.
' --------------------------------------------------------------------------------

Public Function GetFieldCount()
' Specify the default value.
GetFieldCount = 0
' Test if a file is open.
If (blnFileOpen = True) Then
' Test for the number of field names.
If UBound(arrFieldNames) > 0 Then
' Return the count of fields.
GetFieldCount = UBound(arrFieldNames) + 1
End If
End If
End Function

' --------------------------------------------------------------------------------
' Return the specified field's name.
' --------------------------------------------------------------------------------

Public Function GetFieldName(intFieldIndex)
' Specify the default value.
GetFieldName = Null
' Test if a file is open.
If (blnFileOpen = True) Then
' Test if the index is valid.
If intFieldIndex<=UBound(arrFieldNames) Then
' Return the specified field name.
GetFieldName = arrFieldNames(intFieldIndex)
End If
End If
End Function

' --------------------------------------------------------------------------------
' Return the specified field's type.
' --------------------------------------------------------------------------------

Public Function GetFieldType(intFieldIndex)
' Specify the default value.
GetFieldType = Null
' Test if a file is open.
If (blnFileOpen = True) Then
' Test if the index is valid.
If intFieldIndex<=UBound(arrFieldTypes) Then
' Return the specified field type.
GetFieldType = arrFieldTypes(intFieldIndex)
End If
End If
End Function

' --------------------------------------------------------------------------------
' Return the specified field's value.
' --------------------------------------------------------------------------------

Public Function GetValue(intFieldIndex)
' Specify the default value.
GetValue = Null
' Test if a file is open.
If (blnFileOpen = True) Then
' Test if the index is valid.
If intFieldIndex<=UBound(arrCurrentRecord) Then
' Return the specified field value based on the field type.
Select Case arrFieldTypes(intFieldIndex)
Case TYPE_INTEGER:
GetValue = CInt(arrCurrentRecord(intFieldIndex))
Case TYPE_REAL:
GetValue = CDbl(arrCurrentRecord(intFieldIndex))
Case TYPE_STRING:
GetValue = CStr(arrCurrentRecord(intFieldIndex))
Case TYPE_TIMESTAMP:
GetValue = CDate(arrCurrentRecord(intFieldIndex))
Case Else
GetValue = Null
End Select
End If
End If
End Function

' --------------------------------------------------------------------------------
' Read the next record, and return true or false if there is more data.
' --------------------------------------------------------------------------------

' Specify the default value.
' Test if a file is open.
If (blnFileOpen = True) Then
' Test if there is more data.
If objFile.AtEndOfStream Then
' Flag the log file as having no more data.
Else
' Flag the log file as having more data to process.
End If
End If
End Function

</SCRIPT>

</SCRIPTLET>

After you have saved the scriptlet code to your computer, you register it by using the following syntax:

regsvr32 Generic.LogParser.Scriptlet.sct

At the very minimum, you can now use the COM plug-in with Log Parser by using syntax like the following:

logparser "SELECT * FROM 'C:\Foo\Bar.log'" -i:COM -iProgID:Generic.LogParser.Scriptlet

Next, let's analyze what this sample does.

### Examining the Generic Scriptlet in Detail

Here are the different parts of the scriptlet and what they do:

• The <registration> section of the scriptlet sets up the COM registration information; you'll notice the COM component class name and GUID, as well as version information and a general description. (Note that you should generate your own GUID for each scriptlet that you create.)
• The <implements> section declares the public methods that the COM plug-in has to support.
• The <script>section contains the actual implementation:
• The first part of the script section declares the global variables that will be used:
• The strSeparator  constant defines the delimiter that is used to separate the data between fields/columns in a text-based log file.
• The blnHeaderRow  constant defines whether the first row in a text-based log file contains the names of the fields/columns:
• If set to True, the plug-in will use the data in the first line of the log file to name the fields/columns.
• If set to False, the plug-in will define generic field/column names like "Field1", "Field2", etc.
• The second part of the script contains the required methods:
• The OpenInput()  method performs several tasks:
• Locates and opens the log file that you specify in your SQL statement, or returns an error if the log file cannot be found.
• Determines the number, names, and data types of fields/columns in the log file.
• The CloseInput()  method cleans up the session by closing the log file and destroying objects.
• The GetFieldCount()  method returns the number of fields/columns in the log file.
• The GetFieldName()  method returns the name of a field/column in the log file.
• The GetFieldType()  method returns the data type of a field/column in the log file. As a reminder, Log Parser supports the following five data types for COM plug-ins: TYPE_INTEGER, TYPE_REAL, TYPE_STRING, TYPE_TIMESTAMP, and TYPE_NULL.
• The GetValue()  method returns the data value of a field/column in the log file.
• The ReadRecord()  method moves to the next line in the log file. This method returns True if there is additional data to read, or False when the end of data is reached.

Next, let's look at how to use the sample.

### Using the Generic Scriptlet with Log Parser

As a sample log file for this blog, I'm going to use the data in the Sample XML File (books.xml) from MSDN. By running a quick Log Parser query that I will show later, I was able to export data from the XML file into text file named "books.log" that represents an example of a simple log file format that I have had to work with in the past:

id|publish_date|author|title|price
bk101|2000-10-01|Gambardella, Matthew|XML Developer's Guide|44.950000
bk102|2000-12-16|Ralls, Kim|Midnight Rain|5.950000
bk103|2000-11-17|Corets, Eva|Maeve Ascendant|5.950000
bk104|2001-03-10|Corets, Eva|Oberon's Legacy|5.950000
bk105|2001-09-10|Corets, Eva|The Sundered Grail|5.950000
bk106|2000-09-02|Randall, Cynthia|Lover Birds|4.950000
bk107|2000-11-02|Thurman, Paula|Splish Splash|4.950000
bk108|2000-12-06|Knorr, Stefan|Creepy Crawlies|4.950000
bk110|2000-12-09|O'Brien, Tim|Microsoft .NET: The Programming Bible|36.950000
bk111|2000-12-01|O'Brien, Tim|MSXML3: A Comprehensive Guide|36.950000
bk112|2001-04-16|Galos, Mike|Visual Studio 7: A Comprehensive Guide|49.950000

In this example, the data is pretty easy to understand - the first row contains the list of field/column names, and the fields/columns are separated by the pipe ("|") character throughout the log file. That being said, you could easily change my sample code to use a different delimiter that your custom log files use.

With that in mind, let's look at some Log Parser examples.

#### Example #1: Retrieving Data from a Custom Log

The first thing that you should try is to simply retrieve data from your custom plug-in, and the following query should serve as an example:

logparser "SELECT * FROM 'C:\sample\books.log'" -i:COM -iProgID:Generic.LogParser.Scriptlet

The above query will return results like the following:

idpublish_dateauthortitleprice
-----------------------------------------------------------------------------------------
bk101 10/1/2000 0:00:00 Gambardella, Matthew XML Developer's Guide 44.950000
bk102 12/16/2000 0:00:00 Ralls, Kim Midnight Rain 5.950000
bk103 11/17/2000 0:00:00 Corets, Eva Maeve Ascendant 5.950000
bk104 3/10/2001 0:00:00 Corets, Eva Oberon's Legacy 5.950000
bk105 9/10/2001 0:00:00 Corets, Eva The Sundered Grail 5.950000
bk106 9/2/2000 0:00:00 Randall, Cynthia Lover Birds 4.950000
bk107 11/2/2000 0:00:00 Thurman, Paula Splish Splash 4.950000
bk108 12/6/2000 0:00:00 Knorr, Stefan Creepy Crawlies 4.950000
bk109 11/2/2000 0:00:00 Kress, Peter Paradox Lost 6.950000
bk110 12/9/2000 0:00:00 O'Brien, Tim Microsoft .NET: The Programming Bible 36.950000
bk111 12/1/2000 0:00:00 O'Brien, Tim MSXML3: A Comprehensive Guide 36.950000
bk112 4/16/2001 0:00:00 Galos, Mike Visual Studio 7: A Comprehensive Guide 49.950000

 Statistics: ----------- Elements processed: 12 Elements output: 12 Execution time: 0.16 seconds

While the above example works a good proof-of-concept for functionality, it's not overly useful, so let's look at additional examples.

#### Example #2: Reformatting Log File Data

Once you have established that you can retrieve data from your custom plug-in, you can start taking advantage of Log Parser's features to process your log file data. In this example, I will use several of the built-in functions to reformat the data:

logparser "SELECT id AS ID, TO_DATE(publish_date) AS Date, author AS Author, SUBSTR(title,0,20) AS Title, STRCAT(TO_STRING(TO_INT(FLOOR(price))),SUBSTR(TO_STRING(price),INDEX_OF(TO_STRING(price),'.'),3)) AS Price FROM 'C:\sample\books.log'" -i:COM -iProgID:Generic.LogParser.Scriptlet

The above query will return results like the following:

IDDateAuthorTitlePrice
------------------------------------------------------------
bk101 10/1/2000 Gambardella, Matthew XML Developer's Guid 44.95
bk102 12/16/2000 Ralls, Kim Midnight Rain 5.95
bk103 11/17/2000 Corets, Eva Maeve Ascendant 5.95
bk104 3/10/2001 Corets, Eva Oberon's Legacy 5.95
bk105 9/10/2001 Corets, Eva The Sundered Grail 5.95
bk106 9/2/2000 Randall, Cynthia Lover Birds 4.95
bk107 11/2/2000 Thurman, Paula Splish Splash 4.95
bk108 12/6/2000 Knorr, Stefan Creepy Crawlies 4.95
bk109 11/2/2000 Kress, Peter Paradox Lost 6.95
bk110 12/9/2000 O'Brien, Tim Microsoft .NET: The 36.95
bk111 12/1/2000 O'Brien, Tim MSXML3: A Comprehens 36.95
bk112 4/16/2001 Galos, Mike Visual Studio 7: A C 49.95

 Statistics: ----------- Elements processed: 12 Elements output: 12 Execution time: 0.02 seconds

This example reformats the dates and prices a little nicer, and it truncates the book titles at 20 characters so they fit a little better on some screens.

#### Example #3: Processing Log File Data

In addition to simply reformatting your data, you can use Log Parser to group, sort, count, total, etc., your data. The following example illustrates how to use Log Parser to count the number of books by author in the log file:

logparser "SELECT author AS Author, COUNT(Title) AS Books FROM 'C:\sample\books.log' GROUP BY Author ORDER BY Author" -i:COM -iProgID:Generic.LogParser.Scriptlet

The above query will return results like the following:

AuthorBooks
-------------------------
Corets, Eva 3
Galos, Mike 1
Gambardella, Matthew 1
Knorr, Stefan 1
Kress, Peter 1
O'Brien, Tim 2
Ralls, Kim 1
Randall, Cynthia 1
Thurman, Paula 1

 Statistics: ----------- Elements processed: 12 Elements output: 9 Execution time: 0.03 seconds

The results are pretty straight-forward: Log Parser parses the data and presents you with a list of alphabetized authors and the total number of books that were written by each author.

#### Example #4: Creating Charts

You can also use data from your custom log file to create charts through Log Parser. If I modify the above example, all that I need to do is add a few parameters to create a chart:

logparser "SELECT author AS Author, COUNT(Title) AS Books INTO Authors.gif FROM 'C:\sample\books.log' GROUP BY Author ORDER BY Author" -i:COM -iProgID:Generic.LogParser.Scriptlet -fileType:GIF -groupSize:800x600 -chartType:Pie -categories:OFF -values:ON -legend:ON

The above query will create a chart like the following:

I admit that it's not a very pretty-looking chart - you can look at the other posts in my Log Parser series for some examples about making Log Parser charts more interesting.

### Summary

In this blog post and my last post, I have illustrated a few examples that should help developers get started writing their own custom input format plug-ins for Log Parser. As I mentioned in each of the blog posts where I have used scriptlets for the COM objects, I would typically use C# or C++ to create a COM component, but using a scriptlet is much easier for demos because it doesn't require installing Visual Studio and compiling a DLL.

There is one last thing that I would like to mention before I finish this blog; I mentioned earlier that I had used Log Parser to reformat the sample Books.xml file into a generic log file that I could use for the examples in this blog. Since Log Parser supports XML as an input format and it allows you to customize your output, I wrote the following simple Log Parser query to reformat the XML data into a format that I had often seen used for text-based log files:

logparser.exe "SELECT id,publish_date,author,title,price INTO books.log FROM books.xml" -i:xml -o:tsv -headers:ON -oSeparator:"|"

Actually, this ability to change data formats is one of the hidden gems of Log Parser; I have often used Log Parser to change the data from one type of log file to another - usually so that a different program can access the data. For example, if you were given the log file with a pipe ("|") delimiter like I used as an example, you could easily use Log Parser to convert that data into the CSV format so you could open it in Excel:

logparser.exe "SELECT id,publish_date,author,title,price INTO books.csv FROM books.log" -i:tsv -o:csv -headers:ON -iSeparator:"|" -oDQuotes:on

I hope these past few blog posts help you to get started writing your own custom input format plug-ins for Log Parser.

That's all for now. ;-)

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

Posted: Feb 28 2013, 10:34 by Bob | Comments (0)
• Currently 0/5 Stars.
• 1
• 2
• 3
• 4
• 5
Filed under: LogParser | Scripting | XML
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

## Advanced Log Parser Part 6 - Creating a Simple Custom Input Format Plug-In

In Part 4 of this series, I illustrated how to create a new COM-based input provider for Log Parser from a custom input format:

For the sample that I published in that blog, I wrote a plug-in that consumed FTP RSCA events, which is highly structured data, and it added a lot of complexity to my example. In the past ten months or so since I published my original blog, I've had several requests for additional information about how to get started writing COM-based input formats for Log Parser, so it occurred to me that perhaps I could have shown a simpler example to get people started instead of diving straight into parsing RSCA data. ;-)

With that in mind, I thought that I would write a couple of blog posts with simpler examples to help anyone who wants to get started writing custom input formats for Log Parser.

For this blog post, I will show you how to create a very basic COM-based input format provider for Log Parser that simply returns static data; you could use this sample as a template to quickly get up-and-running with the basic concepts. (I promise to follow this blog with another real-world example that is still easier-to-use than my RSCA example.)

### A Reminder about Creating COM-based plug-ins for Log Parser

In the blog that I referred to earlier, I mentioned that a COM plug-in has to support the following public methods:

Method NameDescription
OpenInput Opens your data source and sets up any initial environment settings.
GetFieldCount Returns the number of fields that your plug-in will provide.
GetFieldName Returns the name of a specified field.
GetFieldType Returns the datatype of a specified field.
GetValue Returns the value of a specified field.
CloseInput Closes your data source and cleans up any environment settings.

Once you have created and registered a COM plug-in, you call it by using something like the following syntax:

logparser.exe "SELECT * FROM FOO" -i:COM -iProgID:BAR

In the preceding example, FOO is a data source that makes sense to your plug-in, and BAR is the COM class name for your plug-in.

### Creating a Simple COM plug-in for Log Parser

Once again, I'm going to demonstrate how to create a COM component by using a scriptlet, which I like to use for demos because they are quick to design, they're easily portable, and updates take place immediately since no compilation is required. (All of that being said, if I were writing a real COM plug-in for Log Parser, I would use C# or C++.)

To create the sample COM plug-in, copy the following code into a text file, and save that file as "Simple.LogParser.Scriptlet.sct" to your computer. (Note: The *.SCT file extension tells Windows that this is a scriptlet file.)

<SCRIPTLET>
<registration
Description="Simple Log Parser Scriptlet"
Progid="Simple.LogParser.Scriptlet"
Classid="{4e616d65-6f6e-6d65-6973-526f62657274}"
Version="1.00"
Remotable="False" />
<comment>
EXAMPLE: logparser "SELECT * FROM FOOBAR" -i:COM -iProgID:Simple.LogParser.Scriptlet
</comment>
<implements id="Automation" type="Automation">
<method name="OpenInput">
<parameter name="strValue"/>
</method>
<method name="GetFieldCount" />
<method name="GetFieldName">
<parameter name="intFieldIndex"/>
</method>
<method name="GetFieldType">
<parameter name="intFieldIndex"/>
</method>
<method name="GetValue">
<parameter name="intFieldIndex"/>
</method>
<method name="CloseInput">
<parameter name="blnAbort"/>
</method>
</implements>
<SCRIPT LANGUAGE="VBScript">

Option Explicit

Const MAX_RECORDS = 5
Dim intRecordCount

' --------------------------------------------------------------------------------
' Open the input session.
' --------------------------------------------------------------------------------

Public Function OpenInput(strValue)
intRecordCount = 0
End Function

' --------------------------------------------------------------------------------
' Close the input session.
' --------------------------------------------------------------------------------

Public Function CloseInput(blnAbort)
End Function

' --------------------------------------------------------------------------------
' Return the count of fields.
' --------------------------------------------------------------------------------

Public Function GetFieldCount()
GetFieldCount = 5
End Function

' --------------------------------------------------------------------------------
' Return the specified field's name.
' --------------------------------------------------------------------------------

Public Function GetFieldName(intFieldIndex)
Select Case CInt(intFieldIndex)
Case 0:
GetFieldName = "INTEGER"
Case 1:
GetFieldName = "REAL"
Case 2:
GetFieldName = "STRING"
Case 3:
GetFieldName = "TIMESTAMP"
Case 4:
GetFieldName = "NULL"
Case Else
GetFieldName = Null
End Select
End Function

' --------------------------------------------------------------------------------
' Return the specified field's type.
' --------------------------------------------------------------------------------

Public Function GetFieldType(intFieldIndex)
' Define the field type constants.
Const TYPE_INTEGER   = 1
Const TYPE_REAL      = 2
Const TYPE_STRING    = 3
Const TYPE_TIMESTAMP = 4
Const TYPE_NULL      = 5
Select Case CInt(intFieldIndex)
Case 0:
GetFieldType = TYPE_INTEGER
Case 1:
GetFieldType = TYPE_REAL
Case 2:
GetFieldType = TYPE_STRING
Case 3:
GetFieldType = TYPE_TIMESTAMP
Case 4:
GetFieldType = TYPE_NULL
Case Else
GetFieldType = Null
End Select
End Function

' --------------------------------------------------------------------------------
' Return the specified field's value.
' --------------------------------------------------------------------------------

Public Function GetValue(intFieldIndex)
Select Case CInt(intFieldIndex)
Case 0:
GetValue = 1
Case 1:
GetValue = 1.0
Case 2:
GetValue = "One"
Case 3:
GetValue = Now
Case Else
GetValue = Null
End Select
End Function

' --------------------------------------------------------------------------------
' Read the next record, and return true or false if there is more data.
' --------------------------------------------------------------------------------

intRecordCount = intRecordCount + 1
If intRecordCount <= MAX_RECORDS Then
Else
End If
End Function

</SCRIPT>

</SCRIPTLET>

After you have saved the scriptlet code to your computer, you register it by using the following syntax:

regsvr32 Simple.LogParser.Scriptlet.sct

At the very minimum, you can now use the COM plug-in with Log Parser by using syntax like the following:

logparser "SELECT * FROM FOOBAR" -i:COM -iProgID:Simple.LogParser.Scriptlet

This will return results like the following:

INTEGERREALSTRINGTIMESTAMPNULL
-------------------------------------------
1 1.000000 One 2/26/2013 19:42:12 -
1 1.000000 One 2/26/2013 19:42:12 -
1 1.000000 One 2/26/2013 19:42:12 -
1 1.000000 One 2/26/2013 19:42:12 -
1 1.000000 One 2/26/2013 19:42:12 -

Statistics:
-----------
Elements processed: 5
Elements output: 5
Execution time: 0.01 seconds

Next, let's analyze what this sample does.

### Examining the Sample Scriptlet Contents in Detail

Here are the different parts of the scriptlet and what they do:

• The <registration> section of the scriptlet sets up the COM registration information; you'll notice the COM component class name and GUID, as well as version information and a general description. (Note that you should generate your own GUID for each scriptlet that you create.)
• The <implements> section declares the public methods that the COM plug-in has to support.
• The <script>section contains the actual implementation:
• The OpenInput() method opens your data source, although in this example it only initializes the record count. (Note that the value that is passed to the method will be ignored in this example.)
• The CloseInput() method would normally clean up your session, (e.g. close a data file or database, etc.), but it doesn't do anything in this example.
• The GetFieldCount() method returns the number of data fields in each record of your data, which is static in this example.
• The GetFieldName() method returns the name of a field that is passed to the method as a number; the names are static in this example.
• The GetFieldType() method returns the data type of a field that is passed to the method as a number, which are statically-defined in this example. As a reminder, Log Parser supports the following five data types for COM plug-ins: TYPE_INTEGER, TYPE_REAL, TYPE_STRING, TYPE_TIMESTAMP, and TYPE_NULL.
• The GetValue() method returns the data value of a field that is passed to the method as a number. Once again, the data values are statically-defined in this example.
• The ReadRecord() method moves to the next record in your data set; this method returns True if there is data to read, or False when the end of data is reached. In this example, the method increments the record counter and sets the status based on whether the maximum number of records has been reached.

### Summary

That wraps up the simplest example that I could put together of a COM-based input provider for Log Parser. In my next blog, I'll show how to create a generic COM-based input provider for Log Parser that you can use to parse text-based log files.

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Feb 26 2013, 17:38 by Bob | Comments (0)
• Currently 0/5 Stars.
• 1
• 2
• 3
• 4
• 5
Filed under: LogParser | Scripting
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

## FTP Clients - Part 12: BitKinex

For this installment in my series about FTP clients, I want to take a look at BitKinex 3, which is an FTP client from Barad-Dur, LLC. For this blog I used BitKinex 3.2.3, and it is available from the following URL:

http://www.bitkinex.com/

At the time of this blog post, BitKinex 3 is available for free, and it contains a bunch of features that make it an appealing FTP and WebDAV client.

### BitKinex 3 Overview

When you open BitKinex 3, it shows four connection types (which it refers to as Data Sources): FTP, HTTP/WebDAV, SFTP/SSH, and My Computer. The main interface is analogous to what you would expect in a Site Manager with other FTP clients - you can define new data sources (connections) to FTP sites and websites:

 Fig. 2 - The main BitKinex 3 window.

Creating an FTP data source is pretty straight-forward, and there are a fair number of options that you can specify. What's more, data sources can have individual options specified, or they can inherit from a parent note.

 Fig. 3 - Creating a new FTP data source. Fig. 4 - Specifying the options for an FTP data source.

Once a data source has connected, a child window will open and display the folder trees for your local and remote content. (Note: there are several options for customizing how each data source can be displayed.)

 Fig. 5 - An open FTP data source.

BitKinex 3 has support for command-line automation, which is pretty handy if you do a lot of scripting like I do. Documentation about automating BitKinex 3 from the command line is available on the BitKinex website at the following URL:

BitKinex Command Line Interface

That being said, the documentation is a bit sparse and there are few examples, so I didn't attempt anything ambitious from a command line during my testing.

### Using BitKinex 3 with FTP over SSL (FTPS)

BitKinex 3 has built-in support for FTP over SSL (FTPS) supports both Explicit and Implicit FTPS. To specify the FTPS mode, you need to choose the correct mode from the Security drop-down menu for your FTP data source.

 Fig. 6 - Specifying the FTPS mode.

Once you have established an FTPS connection through BitKinex 3, the user experience is the same as it is for a standard FTP connection.

### Using Using BitKinex 3 with True FTP Hosts

True FTP hosts are not supported natively, and even though BitKinex 3 allows you to send a custom command after a data source has been opened, I could not find a way to send a custom command before sending user credentials, so true FTP hosts cannot be used.

### Using Using BitKinex 3 with Virtual FTP Hosts

BitKinex 3's login settings allow you to specify the virtual host name as part of the user credentials by using syntax like "ftp.example.com|username" or "ftp.example.com\username", so you can use virtual FTP hosts with BitKinex 3.

 Fig. 7 - Specifying an FTP virtual host.

### Scorecard for BitKinex 3

This concludes my quick look at a few of the FTP features that are available with BitKinex 3, and here are the scorecard results:

Client
Name
Directory
Browsing
Explicit
FTPS
Implicit
FTPS
Virtual
Hosts
True
HOSTs
Site
Manager
Extensibility
BitKinex 3.2.3 Rich Y Y Y N Y N/A
Note: I could not find anyway to extend the functionality of BitKinex 3; but as I
mentioned earlier, it does support command-line automation.

That wraps it up this blog - BitKinex 3 is pretty cool FTP client with a lot of options, and I think that my next plan of action is to try out the WebDAV features that are available in BitKinex 3. ;-)

Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
Posted: Jan 31 2013, 15:53 by Bob | Comments (0)
• Currently 0/5 Stars.
• 1
• 2
• 3
• 4
• 5
Filed under: FTP
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us

## Restarting the FTP Service Orphans a DLLHOST.EXE Process

I was recently creating a new authentication provider using FTP extensibility, and I ran into a weird behavior that I had seen before. With that in mind, I thought my situation would make a great blog subject because someone else may run into it.

Here are the details of the situation: let's say that you are developing a new FTP provider for IIS, and your code changes never seem to take effect. Your provider appears to be working, it's just that any new functionality is not reflected in your provider's behavior. You restart the FTP service as a troubleshooting step, but that does not appear to make any difference.

I'll bypass mentioning any other troubleshooting tasks and cut to the chase - if you read my Changing the Identity of the FTP 7 Extensibility Process blog post a year ago, you will recall that I mentioned that all custom FTP extensibility providers are executed through COM+ in a DLLHOST.exe process. When you restart the FTP service, that should clean up the DLLHOST.EXE process that is being used for FTP extensibility. However, if you are developing custom FTP providers and the DLLHOST.EXE process is not terminated by the FTP service, you may find yourself in a situation where you have a DLLHOST.EXE process in memory that contains an older copy of your provider, which will not be removed from memory until the DLLHOST.EXE process for FTP extensibility has been forcibly terminated.

If you have read some of my earlier blog posts or walkthroughs on IIS.NET, you may have noticed that I generally like to use a few pre-build and post-build commands in my FTP projects; usually I add these commands in order to to automatically register/unregister my FTP providers in the Global Assembly Cache (GAC).

With a little modification and some command-line wizardry, you can automate the termination of any orphaned DLLHOST.EXE processes that are being used for FTP extensibility. With that in mind, here are some example pre-build/post-build commands that will unregister/reregister your provider in the GAC, restart the FTP service, and terminate any orphaned FTP extensibility DLLHOST.EXE processes.

Note: The following syntax was written using Visual Studio 2010; you would need to change "%VS100COMNTOOLS%" to "%VS90COMNTOOLS%" for Visual Studio 2008 or "%VS110COMNTOOLS%" for Visual Studio 2012.

Pre-build Commands:

net stop ftpsvc

call "%VS100COMNTOOLS%\vsvars32.bat">nul

cd /d "$(TargetDir)" gacutil.exe /uf "$(TargetName)"

for /f "usebackq tokens=1,2* delims=," %%a in (tasklist /fi "MODULES eq Microsoft.Web.FtpServer.*" /fi "IMAGENAME eq DLLHOST.EXE" /fo csv ^| find /i "dllhost.exe") do taskkill /f /pid %%b

Post-build Commands:

call "%VS100COMNTOOLS%\vsvars32.bat">nul

gacutil.exe /if "$(TargetPath)" net start ftpsvc The syntax is a little tricky for the FOR statement, so be carefully when typing or copying/pasting that into your projects. For example, you need to make sure that all of the code from the FOR statement through the TASKKILL command are on the same line in your project's properties. When you compile your provider, Visual Studio should display something like the following: ------ Rebuild All started: Project: FtpBlogEngineNetAuthentication, Configuration: Release Any CPU ------ 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.1 Copyright (c) Microsoft Corporation. All rights reserved. Assembly: FtpBlogEngineNetAuthentication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73, processorArchitecture=MSIL Uninstalled: FtpBlogEngineNetAuthentication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=426f62526f636b73, processorArchitecture=MSIL Number of assemblies uninstalled = 1 Number of failures = 0 SUCCESS: The process with PID 12656 has been terminated. FtpBlogEngineNetAuthentication -> C:\Users\dude\Documents\Visual Studio 2010\Projects\FtpBlogEngineNetAuthentication\FtpBlogEngineNetAuthentication\bin\Release\FtpBlogEngineNetAuthentication.dll Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.1 Copyright (c) Microsoft Corporation. All rights reserved. Assembly successfully added to the cache The Microsoft FTP Service service is starting. The Microsoft FTP Service service was started successfully. ========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ========== If you analyze the output from the build process, you will see that the commands in my earlier samples stopped the FTP service, removed the existing assembly from the GAC, terminated any orphaned DLLHOST.EXE processes, registered the newly-built DLL in the GAC, and then restarted the FTP service. By utilizing these pre-build/post-build commands, I have been able to work around situations where a DLLHOST.EXE process is being orphaned and caching old assemblies in memory. Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/ Posted: Jan 30 2013, 18:13 by Bob | Comments (0) • Currently 0/5 Stars. • 1 • 2 • 3 • 4 • 5 Filed under: Scripting | FTP | Extensibility Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us ## Using Classic ASP and URL Rewrite for Dynamic SEO Functionality I had another interesting situation present itself recently that I thought would make a good blog: how to use Classic ASP with the IIS URL Rewrite module to dynamically generate Robots.txt and Sitemap.xml files. ### Overview Here's the situation: I host a website for one of my family members, and like everyone else on the Internet, he wanted some better SEO rankings. We discussed a few things that he could do to improve his visibility with search engines, and one of the suggestions that I gave him was to keep his Robots.txt and Sitemap.xml files up-to-date. But there was an additional caveat - he uses two separate DNS names for the same website, and that presents a problem for absolute URLs in either of those files. Before anyone points out that it's usually not a good idea to host multiple DNS names on the same content, there are times when this is acceptable; for example, if you are trying to decide which of several DNS names is the best to use, you might want to bind each name to the same IP address and parse your logs to find out which address is getting the most traffic. In any event, the syntax for both Robots.txt and Sitemap.xml files is pretty easy, so I wrote a couple of simple Classic ASP Robots.asp and Sitemap.asp pages that output the correct syntax and DNS-specific URLs for each domain name, and I wrote some simple URL Rewrite rules that rewrite inbound requests for Robots.txt and Sitemap.xml files to the ASP pages, while blocking direct access to the Classic ASP pages themselves. All of that being said, there are a couple of quick things that I would like to mention before I get to the code: • First of all, I chose Classic ASP for the files because it allows the code to run without having to load any additional framework; I could have used ASP.NET or PHP just as easily, but either of those would require additional overhead that isn't really required. • Second, the specific website for which I wrote these specific examples consists of all static content that is updated a few times a month, so I wrote the example to parse the physical directory structure for the website's URLs and specified a weekly interval for search engines to revisit the website. All of these options can easily be changed; for example, I reused this code a little while later for a website where all of the content was created dynamically from a database, and I updated the code in the Sitemap.asp file to create the URLs from the dynamically-generated content. (That's really easy to do, but outside the scope of this blog.) That being said, let's move on to the actual code. ### Creating the Required Files There are three files that you will need to create for this example: 1. A Robots.asp file to which URL Rewrite will send requests for Robots.txt 2. A Sitemap.asp file to which URL Rewrite will send requests for Sitemap.xml 3. A Web.config file that contains the URL Rewrite rules #### Step 1 - Creating the Robots.asp File You need to save the following code sample as Robots.asp in the root of your website; this page will be executed whenever someone requests the Robots.txt file for your website. This example is very simple: it checks for the requested hostname and uses that to dynamically create the absolute URL for the website's Sitemap.xml file. <% Option Explicit On Error Resume Next Dim strUrlRoot Dim strHttpHost Dim strUserAgent Response.Clear Response.Buffer = True Response.ContentType = "text/plain" Response.CacheControl = "public" Response.Write "# Robots.txt" & vbCrLf Response.Write "# For more information on this file see:" & vbCrLf Response.Write "# http://www.robotstxt.org/" & vbCrLf & vbCrLf strHttpHost = LCase(Request.ServerVariables("HTTP_HOST")) strUserAgent = LCase(Request.ServerVariables("HTTP_USER_AGENT")) strUrlRoot = "http://" & strHttpHost Response.Write "# Define the sitemap path" & vbCrLf Response.Write "Sitemap: " & strUrlRoot & "/sitemap.xml" & vbCrLf & vbCrLf Response.Write "# Make changes for all web spiders" & vbCrLf Response.Write "User-agent: *" & vbCrLf Response.Write "Allow: /" & vbCrLf Response.Write "Disallow: " & vbCrLf Response.End %> #### Step 2 - Creating the Sitemap.asp File The following example file is also pretty simple, and you would save this code as Sitemap.asp in the root of your website. There is a section in the code where it loops through the file system looking for files with the *.html file extension and only creates URLs for those files. If you want other files included in your results, or you want to change the code from static to dynamic content, this is where you would need to update the file accordingly. <% Option Explicit On Error Resume Next Response.Clear Response.Buffer = True Response.AddHeader "Connection", "Keep-Alive" Response.CacheControl = "public" Dim strFolderArray, lngFolderArray Dim strUrlRoot, strPhysicalRoot, strFormat Dim strUrlRelative, strExt Dim objFSO, objFolder, objFile strPhysicalRoot = Server.MapPath("/") Set objFSO = Server.CreateObject("Scripting.Filesystemobject") strUrlRoot = "http://" & Request.ServerVariables("HTTP_HOST") ' Check for XML or TXT format. If UCase(Trim(Request("format")))="XML" Then strFormat = "XML" Response.ContentType = "text/xml" Else strFormat = "TXT" Response.ContentType = "text/plain" End If ' Add the UTF-8 Byte Order Mark. Response.Write Chr(CByte("&hEF")) Response.Write Chr(CByte("&hBB")) Response.Write Chr(CByte("&hBF")) If strFormat = "XML" Then Response.Write "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf Response.Write "<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">" & vbCrLf End if ' Always output the root of the website. Call WriteUrl(strUrlRoot,Now,"weekly",strFormat) ' -------------------------------------------------- ' This following section contains the logic to parse ' the directory tree and return URLs based on the ' static *.html files that it locates. This is where ' you would change the code for dynamic content. ' -------------------------------------------------- strFolderArray = GetFolderTree(strPhysicalRoot) For lngFolderArray = 1 to UBound(strFolderArray) strUrlRelative = Replace(Mid(strFolderArray(lngFolderArray),Len(strPhysicalRoot)+1),"\","/") Set objFolder = objFSO.GetFolder(Server.MapPath("." & strUrlRelative)) For Each objFile in objFolder.Files strExt = objFSO.GetExtensionName(objFile.Name) If StrComp(strExt,"html",vbTextCompare)=0 Then If StrComp(Left(objFile.Name,6),"google",vbTextCompare)<>0 Then Call WriteUrl(strUrlRoot & strUrlRelative & "/" & objFile.Name, objFile.DateLastModified, "weekly", strFormat) End If End If Next Next ' -------------------------------------------------- ' End of file system loop. ' -------------------------------------------------- If strFormat = "XML" Then Response.Write "</urlset>" End If Response.End ' ====================================================================== ' ' Outputs a sitemap URL to the client in XML or TXT format. ' ' tmpStrFreq = always|hourly|daily|weekly|monthly|yearly|never ' tmpStrFormat = TXT|XML ' ' ====================================================================== Sub WriteUrl(tmpStrUrl,tmpLastModified,tmpStrFreq,tmpStrFormat) On Error Resume Next Dim tmpDate : tmpDate = CDate(tmpLastModified) ' Check if the request is for XML or TXT and return the appropriate syntax. If tmpStrFormat = "XML" Then Response.Write " <url>" & vbCrLf Response.Write " <loc>" & Server.HtmlEncode(tmpStrUrl) & "</loc>" & vbCrLf Response.Write " <lastmod>" & Year(tmpLastModified) & "-" & Right("0" & Month(tmpLastModified),2) & "-" & Right("0" & Day(tmpLastModified),2) & "</lastmod>" & vbCrLf Response.Write " <changefreq>" & tmpStrFreq & "</changefreq>" & vbCrLf Response.Write " </url>" & vbCrLf Else Response.Write tmpStrUrl & vbCrLf End If End Sub ' ====================================================================== ' ' Returns a string array of folders under a root path ' ' ====================================================================== Function GetFolderTree(strBaseFolder) Dim tmpFolderCount,tmpBaseCount Dim tmpFolders() Dim tmpFSO,tmpFolder,tmpSubFolder ' Define the initial values for the folder counters. tmpFolderCount = 1 tmpBaseCount = 0 ' Dimension an array to hold the folder names. ReDim tmpFolders(1) ' Store the root folder in the array. tmpFolders(tmpFolderCount) = strBaseFolder ' Create file system object. Set tmpFSO = Server.CreateObject("Scripting.Filesystemobject") ' Loop while we still have folders to process. While tmpFolderCount <> tmpBaseCount ' Set up a folder object to a base folder. Set tmpFolder = tmpFSO.GetFolder(tmpFolders(tmpBaseCount+1)) ' Loop through the collection of subfolders for the base folder. For Each tmpSubFolder In tmpFolder.SubFolders ' Increment the folder count. tmpFolderCount = tmpFolderCount + 1 ' Increase the array size ReDim Preserve tmpFolders(tmpFolderCount) ' Store the folder name in the array. tmpFolders(tmpFolderCount) = tmpSubFolder.Path Next ' Increment the base folder counter. tmpBaseCount = tmpBaseCount + 1 Wend GetFolderTree = tmpFolders End Function %> Note: There are two helper methods in the preceding example that I should call out: • The GetFolderTree() function returns a string array of all the folders that are located under a root folder; you could remove that function if you were generating all of your URLs dynamically. • The WriteUrl() function outputs an entry for the sitemap file in either XML or TXT format, depending on the file type that is in use. It also allows you to specify the frequency that the specific URL should be indexed (always, hourly, daily, weekly, monthly, yearly, or never). #### Step 3 - Creating the Web.config File The last step is to add the URL Rewrite rules to the Web.config file in the root of your website. The following example is a complete Web.config file, but you could merge the rules into your existing Web.config file if you have already created one for your website. These rules are pretty simple, they rewrite all inbound requests for Robots.txt to Robots.asp, and they rewrite all requests for Sitemap.xml to Sitemap.asp?format=XML and requests for Sitemap.txt to Sitemap.asp?format=TXT; this allows requests for both the XML-based and text-based sitemaps to work, even though the Robots.txt file contains the path to the XML file. The last part of the URL Rewrite syntax returns HTTP 404 errors if anyone tries to send direct requests for either the Robots.asp or Sitemap.asp files; this isn't absolutely necesary, but I like to mask what I'm doing from prying eyes. (I'm kind of geeky that way.) <?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <rewrite> <rewriteMaps> <clear /> <rewriteMap name="Static URL Rewrites"> <add key="/robots.txt" value="/robots.asp" /> <add key="/sitemap.xml" value="/sitemap.asp?format=XML" /> <add key="/sitemap.txt" value="/sitemap.asp?format=TXT" /> </rewriteMap> <rewriteMap name="Static URL Failures"> <add key="/robots.asp" value="/" /> <add key="/sitemap.asp" value="/" /> </rewriteMap> </rewriteMaps> <rules> <clear /> <rule name="Static URL Rewrites" patternSyntax="ECMAScript" stopProcessing="true"> <match url=".*" ignoreCase="true" negate="false" /> <conditions> <add input="{Static URL Rewrites:{REQUEST_URI}}" pattern="(.+)" /> </conditions> <action type="Rewrite" url="{C:1}" appendQueryString="false" redirectType="Temporary" /> </rule> <rule name="Static URL Failures" patternSyntax="ECMAScript" stopProcessing="true"> <match url=".*" ignoreCase="true" negate="false" /> <conditions> <add input="{Static URL Failures:{REQUEST_URI}}" pattern="(.+)" /> </conditions> <action type="CustomResponse" statusCode="404" subStatusCode="0" /> </rule> <rule name="Prevent rewriting for static files" patternSyntax="Wildcard" stopProcessing="true"> <match url="*" /> <conditions> <add input="{REQUEST_FILENAME}" matchType="IsFile" /> </conditions> <action type="None" /> </rule> </rules> </rewrite> </system.webServer> </configuration> ### Summary That sums it up for this blog; I hope that you get some good ideas from it. For more information about the syntax in Robots.txt and Sitemap.xml files, see the following URLs: Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/ Posted: Dec 31 2012, 08:09 by Bob | Comments (0) • Currently 0/5 Stars. • 1 • 2 • 3 • 4 • 5 Filed under: IIS | URL Rewrite | SEO | Classic ASP Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us ## Upgrading a Baby Computer I'd like to take a brief departure from my normal series of IIS-related blogs and talk about something very near and dear to the hearts of many geeks - ripping a computer apart and upgrading its various hardware components just because it's fun. ;-) Several years ago I bought a Dell Inspiron Mini 1011 Laptop, which is a smallish netbook computer with a 10-inch screen. (Actually, I bought this as an alternate laptop for my wife to use when travelling, since she doesn't like to travel with her full-sized laptop.) This computer eventually became a "coffee-table laptop" for our house, which houseguests use when they come to visit. Since the netbook computer is so small, our family has affectionately labeled it the "Baby Computer." Recently my wife and I took a trip to Hawaii, for which I decided to leave my full-size laptop at home, and I brought the Baby Computer instead. Since I had never needed to rely on the Baby Computer to do anything more than surf the web in the past, I hadn't realized how quickly it was starved for resources whenever I tried to edit photos or write code. (Yes - I actually write code while on vacation... writing code makes me happy.) The Baby Computer shipped with an underwhelming 1GB of RAM, which filled up quickly if I tried to do too many things at once, and it came with a 120GB 5400rpm hard drive. There's nothing that I could do about CPU speed, but as I slogged through the rest of my vacation using the Baby Computer, I resolved to research if the other hardware in this laptop could be expanded.  Figure 1 - Performance Before Upgrading Once we got home from vacation I did some checking, and I discovered that I could expand the Baby Computer's RAM to 2GB, which isn't much, but it obviously doubled what I had been using, and I decided replace it's original hard drive with a 128GB solid-state drive (SSD). With that in mind, I thought that it would be a worthwhile endeavor to document the upgrade process for someone else who wants to do the same thing with their Dell Inspiron Mini 1011. (Of course, you are undoubtedly voiding your Dell warranty the moment that you open your laptop's case.) First things first - Dell's support website has some great information about tearing apart your laptop; Dell provides a detailed online Service Manual with all of the requisite instructions for replacing most of the parts in the Dell Mini, and I used it as a guide while I performed my upgrades. That being said, the upgrade process was still a little tricky, and some of the parts were difficult to get to. (Although it seems like Dell may have made upgrades a little easier in later models of my laptop.) So without further introduction, here are the steps for upgrading the RAM and hard drive in a Dell Inspiron Mini 1011 Laptop. #### Step 1 - Remove the Screws from the Back of the Case This step is pretty easy - there are only a handful of screws to remove.  Figure 2 - Removing the Screws #### Step 2 - Remove the Keyboard It's pretty easy to pop the keyboard out of the case...  Figure 3 - Removing the Keyboard ...although once you have the keyboard loose, you need to flip it over carefully and remove the flat ribbon cable from underneath.  Figure 4 - Detaching the Keyboard Cable #### Step 3 - Remove the Palm Rest This step was a little tricky, and it took me a while to accomplish this task because I had to wedge a thin screwdriver in between the case and the palm rest in order to pry it off. Note that there is a flat ribbon cable that attaches the palm rest to the motherboard that you will need to remove.  Figure 5 - Removing the Palm Rest With the keyboard and palm rest out of the way, you can remove the hard drive - there's a single screw holding the hard drive mount into the case and four screws that hold the hard drive in its mount.  Figure 6 - Removing the Hard Drive If you were only replacing the hard drive, you could stop here. Since I was upgrading the RAM, I needed to dig deeper. #### Step 4 - Remove the Palm Rest Bracket and Motherboard Once the hard drive is out of the way, you need to remove the motherboard so you can replace the RAM that is located underneath it. There are a handful of screws above and below the computer that hold the palm rest bracket to the case...  Figure 7 - After Removing the Palm Rest Bracket ...once you remove remove the palm rest bracket, you can flip over the motherboard and replace the RAM.  Figure 8 - Replacing the RAM #### Optional Step - Cloning the Hard Drive Rather than reinstalling the operating system from scratch, I cloned Windows from the original hard drive to the SSD. To do this, I placed both the old hard drive and the new SSD into USB-based SATA drive cradles and I used Symantec Ghost to clone the operating system from drive to drive.  Figure 9 - Both Hard Drives in SATA Cradles Figure 10 - Cloning the Hard Drive with Ghost Once the clone was completed, all that was left was to install the new SSD and reassemble the computer.  Figure 11 - Installing the New SSD #### Summary Once I had everything completed and reassembled, Windows booted considerably faster when using the SSD; it now boots in a matter of seconds. (I wish that I had timed the boot sequence before and after the upgrades, but I didn't think of that earlier... darn.) Running the Windows 7 performance assessment showed a measurable increase in hard drive speed, with little to no increase in RAM speed. Of course, since there was no speed increase for CPU or graphics, the overall performance score for my laptop remained the same. That being said, with twice the RAM as before, it should be paging to disk less often, so regular usage should seem a little faster; even when it does need to swap memory to disk it will be faster using the SSD than with its old hard drive.  Figure 12 - Performance After Upgrading That's all for now - have fun. ;-) Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/ Posted: Dec 28 2012, 07:53 by bob | Comments (0) • Currently 0/5 Stars. • 1 • 2 • 3 • 4 • 5 Filed under: Hardware Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us ## Why I Personally Think the Zune Failed First and foremost - I am not ashamed to admit that I am a card-carrying Zune fanboy. But that being said, as a faithful owner of several Zune devices, I am ashamed of the way that the Zune team at Microsoft so badly botched their product line; the Zune team was so out of touch with their target consumers that it borders on negligence. Here is my totally-biased list of reasons why I personally think the Zune failed. ### My Top 10 Reasons Why the Zune Failed #### Reason #1 - Microsoft entered the game with TOO LITTLE TOO LATE There were a smattering of MP3 players on the market by the time that Apple's iPod hit the stores. I still have an RCA Lyra device that kicked butt in its day, but my personal favorites were the Creative Zen devices; you plugged a Zen player into your computer and it showed up like an external hard drive. To add music, you simply dragged & dropped music files anywhere you wanted; the Zen devices used your music files' metadata to sort by albums, genres, artists, etc. When Apple's iPod hit the stores, its main rise to fame was its end-to-end story from iTunes to iPod, all of which belonged to Apple. Their devices were cool, and their advertising was stellar (as always). Even though they were overpriced, the iPod soon became "the product" that everyone wanted. The iTunes/iPod integration was closed to outsiders, which meant that Apple owned the end-to-end experience, and thereby collected all of the profits from it. When Microsoft eventually realized that Apple was making enough money off their music/devices sales to save their company - which was formerly close to bankruptcy - they decided to create a device and end-to-end experience for themselves. But when Microsoft tried to do so, they mostly opted for feature parity with iTunes. What was Microsoft thinking? Instead of improving on the iTunes model, they were trying to break into an established market with a product that had little to offer that was above and beyond what consumers could already get. FAIL. #### Reason #2 - The early Zune end-to-end experience was terrible I bought my wife a Zune for Christmas when they first released. Having owned and used several MP3 players in the past, I thought that it would be a similar experience; let me assure you, it was decidedly not a similar experience. I was so frustrated with the first-generation Zune software that I had boxed up the Zune and was ready to take it back to the store within an hour of trying to get it set up for her. I eventually elected not to do so, and I managed to get it working, but it was a crappy experience that made me apologize to my non-technical wife for burdening her with such a mess. FAIL. FAIL. FAIL. #### Reason #3 - You needed to use the Zune software to put files on the device Customers wanted to use their Zune devices as external storage, but having to use the Zune software to transfer files to the device prevented that. The prevailing argument was that Zune followed the iTunes/iPod model, but who cares if that's the way that iTunes/iPod worked? Zune customers paid good money for their devices, and they wanted to store files on those. USB flash drives were still pretty pricey at the time, so opening the Zune platform to double as external storage would have been a fantastic selling feature, but that concept escaped the Zune team's leadership because they wanted to force users into having to use their @#$% software in the hopes that they would be tempted to buy more music/videos through Microsoft.

DESIGN FAIL.

[On a related note, the Windows Phone 7 team did not learn from the Zune's failure, and their devices still had the same, stupid Zune software requirement. BRAIN-DEAD FAIL.]

#### Reason #4 - You couldn't use Windows Media Player with the the Zune

Microsoft already made a killer media player application for Windows that worked with all the third-party MP3 player devices, but when Microsoft introduced their own MP3 player it didn't work with their existing Windows Media Player.

EPIC FAIL.

#### Reason #5 - Zune Didn't Support Plays For Sure

Microsoft spent a bunch of money cozying up to the music industry and MP3 makers with a program that was entitled Plays For Sure, whereby devices could be certified to play all Windows-based music files, whether they had copy protection on them or not. Even though all of these third-party companies went through the certification process, Microsoft's own player didn't have to; the Zune didn't support Plays for Sure.

[On a related note, this probably hastened the demise of WMA as a file format. SHOOT YOURSELF IN THE FOOT FAIL.]

#### Reason #6 - The Zune Software for Windows Sucked

For a long time - and I mean a really long time - the software that you needed to use with your Zune was next to worthless. It was slow, buggy, and ugly. By the time that the Zune team finally delivered a version of the Zune software that was actually worth installing, the battle for MP3 player supremacy was over and the iPod ruled uncontested.

#### Reason #7 - Dropping the free downloads from the Zune Pass

The Zune pass tried to be the Netflix service of music, and in that sense it was ahead of the curve when it was introduced. Customers paid $14.95 a month, and in exchange they were granted free access to tens of thousands of DRM-based WMA music files - all of which they could download and play on their computers or Zune devices - and customers could play them as long as they kept their Zune pass up-to-date. In addition, customers got to keep 10 free songs a month in DRM-free MP3 format per month. In September, 2011, some wunderkind in the Zune group decided to take away the 10 free songs and drop the price of the Zune pass to$9.99 per month. This person - whoever they may be - is an idiot. With the incredible amount of free music that is available on the Internet now, the free downloads on the Zune pass was the only feature of the Zune pass that made having a Zune pass worthwhile.

#### Reason #8 - Zune Required Users to Buy Music with 'Points'

The rest of the world works with actual money, but the Zune service required customers to use 'points' to buy music or videos, and points did not map directly to dollars and cents. On Amazon or iTunes, music was typically $0.99 per track, but on Zune it was typically 89 points per track. WTF? What the @#$% was a 'point'?

So let's say that you wanted to buy a music file to download; the Zune software would inform you that had to buy points first, which would have some weird exchange rate that didn't make sense. For example, if you bought 400 points for $4.99, that would mean that your 89-point music file actually cost$1.11, which was \$0.12 more than Amazon or iTunes. When Microsoft combined their crappy points-based purchasing system with their overpriced music, they created an environment that was a truly horrible customer experience.

WTF FAIL.

#### Reason #9 - It Took Too Long to Market Zune Overseas

The iPod was dominating media player sales all over the world, but believe it or not - there are people that simply don't like Apple. I constantly saw people all over Europe that were clamoring for a Zune, and Microsoft didn't deliver.

#### Reason #10 - No Zune Software for the Mac

Believe it or not, I saw a lot of Mac users who were asking for Zune software on the Mac. I'm not sure if these people were also iTunes users or not, but I think that the concept of the original "10 free songs" with a Zune pass was appealing to them. Sadly, Microsoft did not deliver - and a whole slew of potential customers were left high-and-dry.

### In Closing

Despite all this negativism, the Zune team did deliver some great products - I still own several Zunes from the various series of players, although it now feels a lot like owning a Tucker automobile or a Betamax VCR.

Here are some of the coolness factors that Zune had:

• Wireless Networking - when the first Zunes came out, they were the first players to have wireless networking, which allowed Zune users to share files. I remember at the time that some of my Apple fanboy friends remarked that this was a stupid feature; their usual mantra was: "Who wants networking in a handheld device?" Seriously - I got asked that a lot. Now most every player worth its mettle has Wi-Fi support, as do iPhones, Windows Phones, etc. Zune was well-ahead of the curve.
• Larger Storage at Cheaper Prices - for many versions of the Zune, you would get the same features in your Zune for a lot less money than a comparable iPod. The Zune didn't catch on, but that certainly wasn't due to price-per-feature.
• Larger & Better Screens - when the Zune first came out, it's screen was much larger and better than its competitors' screens.
• HDTV Support - the Zune HD was a great device, and one of the really cool features was output to HDTV from a really tiny device.

In the end, it was very sad for me to see the Zune fail; the Zune was simply a victim of being superior device with inferior product management.

Posted: Dec 22 2012, 22:26 by Bob | Comments (0)
• Currently 0/5 Stars.
• 1
• 2
• 3
• 4
• 5
Filed under: Zune
Tags:
Social Bookmarks: E-mail | Kick it! | DZone it! | del.icio.us