Monday, February 16, 2009

More PowerShell Adventures

The journey to administration of IIS 7 on Windows Web Server 2008 R2 Core has not been a happy time. Hopefully perseverance will eventually pay some dividends. So my objective right now is to install the IIS 7 PowerShell snap-in. I know where it is: http://66.129.69.178/downloads/files/powershell/iis7psprov_x64_rc1.msi. I just need to download it to my server. Note my server doesn't currently have a DNS entry which means providing a hostname is just not going to work, I need to replace www.iis.net with the equivalent IP Address 66.129.69.178. Google throws up a number of wget scripts; after trying various versions and failing to get anything to retrieve the file I went back to basics and used a page from B#.Net Blog. This uses .Net Framework class System.Net.WebClient to retrieve the file. The problem with most of the scripts I found is that error reporting is generally left to the default PowerShell Exception report which by and large is not particularly useful:



Exception calling "DownloadFile" with "2" argument(s): "An exception occurred during a WebClient request."
At line:1 char:21
+ $client.DownloadFile <<<< ($url, $file)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

In order to get a little more detail I need to trap the exception and delve into it to find out what caused it. This is relatively straight forward using the trap mechanism. To begin with lets display the methods available on the Exception object.



PS C:\Users\Administrator> trap { $error[0].Exception | get-member } $client.DownloadFile($url,$file)

TypeName: System.Management.Automation.MethodInvocationException

Name MemberType Definition
---- ---------- ----------
Equals Method System.Boolean Equals(Object obj)
GetBaseException Method System.Exception GetBaseException()
GetHashCode Method System.Int32 GetHashCode()
GetObjectData Method System.Void GetObjectData(SerializationInfo info, StreamingContext c...
GetType Method System.Type GetType()
ToString Method System.String ToString()
Data Property System.Collections.IDictionary Data {get;}
ErrorRecord Property System.Management.Automation.ErrorRecord ErrorRecord {get;}
HelpLink Property System.String HelpLink {get;set;}
InnerException Property System.Exception InnerException {get;}
Message Property System.String Message {get;}
Source Property System.String Source {get;set;}
StackTrace Property System.String StackTrace {get;}
TargetSite Property System.Reflection.MethodBase TargetSite {get;}
Exception calling "DownloadFile" with "2" argument(s): "An exception occurred during a WebClient re
quest."
At line:1 char:63
+ trap { $error[0].Exception | get-member } $client.DownloadFile <<<< ($url,$file)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

The ToString method is reasonably useful but generates a fair amount of output, including a full stack trace. Time to see what it looks like:



PS C:\Users\Administrator> $client = new-object System.Net.WebClient
PS C:\Users\Administrator> $url = "http://66.129.69.178/downloads/files/powershell/iis7psprov_x64_rc1.msi"
PS C:\Users\Administrator> $file = Join-Path $home Downloads\iis7psprov_x64_rc1.msi
PS C:\Users\Administrator> $file
C:\Users\Administrator\Downloads\iis7psprov_x64_rc1.msi
PS C:\Users\Administrator> trap { $error[0].Exception.ToString() } $client.DownloadFile($url,$file)
System.Management.Automation.MethodInvocationException: Exception calling "DownloadFile" with "2" a
rgument(s): "An exception occurred during a WebClient request." ---> System.Net.WebException: An ex
ception occurred during a WebClient request. ---> System.Configuration.ConfigurationErrorsException
: Error creating the Web Proxy specified in the 'system.net/defaultProxy' configuration section. --
-> System.DllNotFoundException: Unable to load DLL 'rasapi32.dll': The specified module could not b
e found. (Exception from HRESULT: 0x8007007E)
at System.Net.UnsafeNclNativeMethods.RasHelper.RasEnumConnections(RASCONN[] lprasconn, UInt32& l
pcb, UInt32& lpcConnections)
[full stack trace elided]
at System.Management.Automation.StatementListNode.ExecuteStatement(ParseTreeNode statement, Arra
y input, Pipe outputPipe, ArrayList& resultList, ExecutionContext context)
Exception calling "DownloadFile" with "2" argument(s): "An exception occurred during a WebClient re
quest."
At line:1 char:61
+ trap { $error[0].Exception.ToString() } $client.DownloadFile <<<< ($url,$file)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

As I said, a fair amount of output. But it does contain some useful information - Error creating the Web Proxy specified in the 'system.net/defaultProxy' configuration section. ---> System.DllNotFoundException: Unable to load DLL 'rasapi32.dll'. I guess I should have read the release notes:



If you run applications that use managed code that communicates with the Internet with autoproxy
detection, the operation will fail with an unhandled exception that mentions an error creating the
Web proxy because Rasapi32.dll could not be found.

To correct this, open the Machine.config file (default location is
C:\Windows\Microsoft.NET\Framework64\v2.0.50727\CONFIG), locate the final closing </configuration>
tag, and append the following:

<system.net> <defaultProxy> <proxy usesystemdefault="false" proxyaddress="<replace_with_proxy_address>"
bypassonlocal="true" /> </defaultProxy> </system.net>

where <replace_with_proxy_address> is the address and port, if needed, of the proxy server
used by the client application to access the .NET application. For example, proxyaddress="<http://proxyserver:80>".

I'm not actually using a proxy server, what I want to do is avoid the system trying to load the rasapi32.dll. What I need to do is somehow indicate that I don't wish to use the default proxy, whatever that is defined as, when I'm making my WebClient.DownloadFile() request. The correct sequence of calls is then:



PS C:\Users\Administrator> $client = new-object System.Net.WebClient
PS C:\Users\Administrator> $url = "http://66.129.69.178/downloads/files/powershell/iis7psprov_x64_rc1.msi"
PS C:\Users\Administrator> $file = Join-Path $home Downloads\iis7psprov_x64_rc1.msi
PS C:\Users\Administrator> [System.Net.GlobalProxySelection]::Select = [System.Net.GlobalProxySelection]::GetEmptyWebProxy()
PS C:\Users\Administrator> trap { $error[0].Exception.ToString() } $client.DownloadFile($url,$file)

This next step ends up just being stupid, annoying, insulting, you get the picture, not happy. Installing the IIS 7.0 PowerShell Snap-in effectively says "go get the installer and run the installer". So I've finally got the installer. I go ahead and run the installer and the installation fails with an error IIS Version 7.0 or greater is required to install Microsoft Windows PowerShell provider for IIS 7.0. I'm running Windows Server 2008 r2, I've got IIS 7 installed. A forum post adds fuel to the fire, particularly this post:



This is "by design". What you have in Win7 beta is practically the same code as RC of powershell snapin, which is for downlevel versions only.

Really sucks. If something like this is "by design" then it should be documented somewhere obvious. This is just a waste of time for anyone who is trying to get enthusiastic about using this stuff. The workaround is also documented in the thread (thanks 13xforever), change minor version of IIS from 5 to 0 in HKLM:\SOFTWARE\Microsoft\InetStp (and back again after the installation is complete). Now I need to let PowerShell run scripts:



PS C:\Users\Administrator> Get-ExecutionPolicy
Restricted
PS C:\Users\Administrator> Set-ExecutionPolicy RemoteSigned

And to make my life easier on Core I've created $home\Bin\IIS.cmd containing the Target: for the IIS PowerShell Management Console menu item shortcut installed with the snap-in. Unfortunately I can't post that information because blogger thinks I'm doing something dodgy and removes the offending code and the rest of the post (I guess if you've read this far you might consider that a bonus). I've added the Bin folder to the PATH environment variable. To avoid adding the Administrator's Bin folder to the path for all users it's first necessary to locate the appropriate registry key for the Administrator's environment variables. On my server it looks something like HKEY_USERS\S-1-5-21-1234567890-1234567890-1234567890-500\Environment. Then it's just a case of adding a new Expandable String Value called PATH and giving it a value of %PATH%;%UserProfile%\Bin. The command prompt needs to be restarted in order for the change to take effect.


To cap off a fairly unproductive day, I've just been reminded that I shouldn't be using the Administrator account for administration. Absolutely correct and in fact the Administrator account should be renamed and disabled. Way back in Installing Windows Server 2008 Core part 2 I created another administrative account. Time to start using it. This means I need to move my Bin directory and apply the registry changes to a different user profile. Finally:



C:\Users\dave>iis
PS IIS:\>