Showing posts with label WMI. Show all posts
Showing posts with label WMI. Show all posts

Thursday, June 5, 2008

Scripting - Create a desktop shortcut to a URL (Updated for IE7)

This article discusses a script that creates a desktop shortcut to a particular URL, with a given custom icon. I know, pretty routine stuff, but when the requirement of a project is to deploy a 'desktop shortcut' to affected customers, I often find that deploying such a script on a web site, such that it can be accessed on demand, or pushing out via a login script or other type of automated technology meets the requirement.

The script:


  1. Copies a custom icon from a network share to the local computer (so the icon shows up, even if the user is offline)

  2. Creates a shortcut on the desktop, references the custom icon

  3. Prompts the user if they wish to launch the site immediately



I developed this script a number of years ago, and recently had the opportunity to re-use it for another app. Unfortunately, upon initial run, it worked except for the custom icon, it was using the default browser icon instead. Thought maybe I broke something because I had also modified the target location of the custom icon to support Vista (as well as 9x-XP). I then tested versions of it I had written in the past that were still in production, those had the same issue.

Turns out the issue was on computers with Internet Explorer 7. Internet Explorer 6 computers were fine. The IE7 computer had a slightly different format in the *.url files placed on the desktop. Didn't discover this until I opened up the .url file in notepad to compare one the script created, to one that was manually created.

In IE6, the url's were basically this format:



[InternetShortcut]
URL=http://intranet/MyApp
IconIndex=0
IconFile=C:\Documents and Settings\TestUser\Application Data\MyApp.ico

While it is in ini file format (note the section 'InternetShortcut' in square brackets), there was only one section, so we were able to get away with simply appending the property=value lines to the file, as in the following snippent



Dim fso
Set fso = WScript.createobject("Scripting.FileSystemObject")

Dim f
Set f = fso.getfile(sURLLinkFile)

Dim contents
contents = ""

Dim line

Dim ots
Set ots = f.openastextstream(1)

'read in contents of the file, except for any existin icon settings
do while not ots.atEndofstream
line = ots.readline
if instr(1,line,"IconIndex",1) = 0 and instr(1,line,"IconFile",1) = 0 then
contents = contents & line & vbcrlf
end if
loop
ots.close

'add the lines for iconindex and file
if not (isnull(iIconIndex) or isnull(IconLocation)) then
if not (isempty(iIconIndex) or isempty(IconLocation)) then
contents=contents &"IconIndex=" & cstr(iIconIndex) & vbcrlf
contents=contents & "IconFile=" & IconLocation
end if
end if

set ots = f.openastextstream(2)
ots.write contents
ots.close

set ots = nothing
set f = nothing
set fso = nothing

The IE7 version has added a section (notice the GUID in square brackets on the 4th line) and a few more properties


So, absent any change in the script, the .URL files were coming out as:



[InternetShortcut]
URL=http://intranet/MyApp
IDList=
HotKey=0
[{000214A0-0000-0000-C000-000000000046}]
Prop3=19,2
IconIndex=0
IconFile=C:\Documents and Settings\TestUser\Application Data\MyApp.ico

With the Icon reference essentially in the 'wrong' section, no .url appears with the standard browser icon. This appeared to be true regardless of default browser (i.e. FireFox, etc.) just having IE7 vs. IE6. (Computers that had created shortcuts while on IE6, then upgrading to IE7, appeared OK.)


To resolve, grabbed some INI read-write functions from http://www.motobit.com, which, after including the functions, cleaned up the core code quite a bit anyway:



WriteINIString "InternetShortcut", "IconIndex", iIconIndex, sURLLinkFile
WriteINIString "InternetShortcut", "IconFile", IconLocation, sURLLinkFile

So we end up with:



[InternetShortcut]
URL=http://intranet/MyApp
IDList=
HotKey=0
IconIndex=0
IconFile=C:\Documents and Settings\TestUser\Application Data\MyApp.ico
[{000214A0-0000-0000-C000-000000000046}]
Prop3=19,2

Excellent stuff, right? So here's the full code:



'-------------------------------------------------------------------------------
'Creates a desktop 'shortcut', or 'icon' to a given URL
'Runs with limited-rights user, so you can give users the ability to create a
'shortcut on-demand, or roll out with a login script or similar automated device
' Author Chris Anderson cander@realworldis.com
' - Modified the script to drop the custom icon, if specified into the current users 'app data' folder
' more logo-compliant than the old version which hard-coded c:\program files\. it will now run under a
' limited rights user
' - Added prompt to launch the site after creating the shortcut
' - Modified the shortcut creation to be compatible with IE7, the previous version would create the shortcut
' successfully, but the custom icon would not be used.
'-------------------------------------------------------------------------------
Option Explicit

Const URL_SHORTCUT_NAME = "My Intranet Application"
Const TARGET_PATH = "http://intranet/MyApp"
Const ICON_REMOTE_FILE_PATH = "\\server\public\MyApp\MyApp.ico"

'the icon index can allow you to reference an ico resource in the file with many resources (i.e. and .exe or .dll)
'if using a seperate .ico file, set to 0
Dim iIconIndex
iIconIndex = 0

Dim sLocalIconFilePath

'copy icon locally so it shows up regardless of network connection
'logo-compliancy - copy the file to the current users 'Application Data' as they should have write access even when not-admin
CopyIconToLocalApplicationData ICON_REMOTE_FILE_PATH, sLocalIconFilePath

'the script then creates a shortcut on their desktop, to the target url, with the local path of the icon
CreateURLShortcut URL_SHORTCUT_NAME, TARGET_PATH, sLocalIconFilePath

'prompt the user to open the shortcut now
Dim sMessage
sMessage = "There is now an icon on your desktop for " & URL_SHORTCUT_NAME & ". Would you like to open it now?"

if MsgBox(sMessage,vbYesNo) = vbYes then
OpenPath TARGET_PATH
end if

Private Sub CreateURLShortcut(ByVal ShortcutName, Byval TargetPath, ByVal IconLocation)

Dim WSHShell
Set WSHShell = createobject("wscript.shell")

Dim sDesktop
sDesktop= WSHShell.specialfolders("Desktop")

Dim oURLLink
Set oURLLink = WSHShell.createshortcut(sDesktop & "\" & ShortcutName & ".url")
oURLLink.targetpath = TargetPath
oURLLink.save

Dim sURLLinkFile
sURLLinkFile = oURLLink.FullName

'set the icon file
if Not IsEmpty(iIconIndex) AND NOT IsEmpty(IconLocation) Then

'WScript.Echo IconLocation

WriteINIString "InternetShortcut", "IconIndex", iIconIndex, sURLLinkFile
WriteINIString "InternetShortcut", "IconFile", IconLocation, sURLLinkFile

end if

set oURLLink = nothing
set WSHShell = nothing

End Sub

Private Sub CopyIconToLocalApplicationData(Byval RemotePath, ByRef LocalPath)

Dim WSHShell
Set WSHShell = WScript.CreateObject("WScript.Shell")

Dim fso
Set fso = CreateObject("Scripting.filesystemobject")

Dim objFile
Set objFile = fso.GetFile(RemotePath)

'local path will be directly under the users app data
LocalPath = WSHShell.ExpandEnvironmentStrings("%AppData%") & "\" & fso.GetFileName(objFile)

fso.CopyFile RemotePath, LocalPath

Set fso = Nothing

Set WSHShell = Nothing

End Sub

private sub OpenPath(byval TargetPath)

Dim WSHShell
Set WSHShell = createobject("shell.application")

WSHShell.Open TargetPath

set WSHShell = nothing

end sub


'----------------------------------------------------------------
'INI File Handling

'Work with INI files In VBS (ASP/WSH)
'v1.00
'2003 Antonin Foller, PSTRUH Software, http://www.motobit.com
'Function GetINIString(Section, KeyName, Default, FileName)
'Sub WriteINIString(Section, KeyName, Value, FileName)
'----------------------------------------------------------------

Sub WriteINIString(Section, KeyName, Value, FileName)
Dim INIContents, PosSection, PosEndSection

'Get contents of the INI file As a string
INIContents = GetFile(FileName)

'Find section
PosSection = InStr(1, INIContents, "[" & Section & "]", vbTextCompare)
If PosSection>0 Then
'Section exists. Find end of section
PosEndSection = InStr(PosSection, INIContents, vbCrLf & "[")
'?Is this last section?
If PosEndSection = 0 Then PosEndSection = Len(INIContents)+1

'Separate section contents
Dim OldsContents, NewsContents, Line
Dim sKeyName, Found
OldsContents = Mid(INIContents, PosSection, PosEndSection - PosSection)
OldsContents = split(OldsContents, vbCrLf)

'Temp variable To find a Key
sKeyName = LCase(KeyName & "=")

'Enumerate section lines
For Each Line In OldsContents
If LCase(Left(Line, Len(sKeyName))) = sKeyName Then
Line = KeyName & "=" & Value
Found = True
End If
NewsContents = NewsContents & Line & vbCrLf
Next

If isempty(Found) Then
'key Not found - add it at the end of section
NewsContents = NewsContents & KeyName & "=" & Value
Else
'remove last vbCrLf - the vbCrLf is at PosEndSection
NewsContents = Left(NewsContents, Len(NewsContents) - 2)
End If

'Combine pre-section, new section And post-section data.
INIContents = Left(INIContents, PosSection-1) & _
NewsContents & Mid(INIContents, PosEndSection)
else'if PosSection>0 Then
'Section Not found. Add section data at the end of file contents.
If Right(INIContents, 2) <> vbCrLf And Len(INIContents)>0 Then
INIContents = INIContents & vbCrLf
End If
INIContents = INIContents & "[" & Section & "]" & vbCrLf & _
KeyName & "=" & Value
end if'if PosSection>0 Then
WriteFile FileName, INIContents
End Sub

Function GetINIString(Section, KeyName, Default, FileName)
Dim INIContents, PosSection, PosEndSection, sContents, Value, Found

'Get contents of the INI file As a string
INIContents = GetFile(FileName)

'Find section
PosSection = InStr(1, INIContents, "[" & Section & "]", vbTextCompare)
If PosSection>0 Then
'Section exists. Find end of section
PosEndSection = InStr(PosSection, INIContents, vbCrLf & "[")
'?Is this last section?
If PosEndSection = 0 Then PosEndSection = Len(INIContents)+1

'Separate section contents
sContents = Mid(INIContents, PosSection, PosEndSection - PosSection)

If InStr(1, sContents, vbCrLf & KeyName & "=", vbTextCompare)>0 Then
Found = True
'Separate value of a key.
Value = SeparateField(sContents, vbCrLf & KeyName & "=", vbCrLf)
End If
End If
If isempty(Found) Then Value = Default
GetINIString = Value
End Function

'Separates one field between sStart And sEnd
Function SeparateField(ByVal sFrom, ByVal sStart, ByVal sEnd)
Dim PosB: PosB = InStr(1, sFrom, sStart, 1)
If PosB > 0 Then
PosB = PosB + Len(sStart)
Dim PosE: PosE = InStr(PosB, sFrom, sEnd, 1)
If PosE = 0 Then PosE = InStr(PosB, sFrom, vbCrLf, 1)
If PosE = 0 Then PosE = Len(sFrom) + 1
SeparateField = Mid(sFrom, PosB, PosE - PosB)
End If
End Function


'File functions
Function GetFile(ByVal FileName)
Dim FS: Set FS = CreateObject("Scripting.FileSystemObject")
'Go To windows folder If full path Not specified.
If InStr(FileName, ":\") = 0 And Left (FileName,2)<>"\\" Then
FileName = FS.GetSpecialFolder(0) & "\" & FileName
End If
On Error Resume Next

GetFile = FS.OpenTextFile(FileName).ReadAll
End Function

Function WriteFile(ByVal FileName, ByVal Contents)

Dim FS: Set FS = CreateObject("Scripting.FileSystemObject")
'On Error Resume Next

'Go To windows folder If full path Not specified.
If InStr(FileName, ":\") = 0 And Left (FileName,2)<>"\\" Then
FileName = FS.GetSpecialFolder(0) & "\" & FileName
End If

Dim OutStream: Set OutStream = FS.OpenTextFile(FileName, 2, True)
OutStream.Write Contents
End Function

Tuesday, May 20, 2008

PowerShell - Converting a script

As a further exercise with PowerShell, I've converted the 'resetLastLogin' script (http://www.microsoft.com/technet/scriptcenter/csc/scripts/desktop/settings/cscds075.mspx) from VBScript to PowerShell. The script resets the 'default user name' on the specified computer. Basically, if you as an admin logged in to someone's computer, it may show your name as the last to login, confusing the regular user of the computer. This script blanks it out, or sets it to the given text. The script can be ran against a remote computer as well, so as to reset the last login from a remote desktop session as well.

Analysis



The first batch of lines in the VBScript handle the arguments, with appropriate defaults:

Dim sComputer
Dim sNewUserName

if WScript.arguments.count > 0 then
sComputer = WScript.arguments(0)
if WScript.arguments.count > 1 then
sNewUserName = WScript.arguments(1)
else
'default the user name to .
sNewUserName = ""
end if
else
'default computer name to local
sComputer = "."
sNewUserName = ""
end if


This was converted using the parameter definition

param ([string]$MachineName, [string]$DefaultUserName)

and applying some defaults

if ($MachineName -eq "") {$MachineName = $env:COMPUTERNAME}
if ($DefaultUserName -eq "") {$DefaultUserName = ''}

while the overall code is much cleaner, had to make non-intuitive use of the "-eq" operator for comparison, vs. just an '=' sign. I could have left the "[string]" definition off of the parameters, the script worked fine, but when I later went to echo those values back, they were rendering as blank.

VBScript registry access was similar in that we first get a registry object:

'get the registry object
Set oReg=GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _
sComputer & "\root\default:StdRegProv")

get the old value

oReg.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

and set the new value

oReg.SetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, sNewUserName


In PowerShell, the O-O pattern means that we follow a pattern of getting an object representing the registry:

$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $MachineName)

, get an object representing the subkey

$regKey = $reg.OpenSubKey($keyPath,$true)

then get the old value (only to display later) and set the new value

$oldValue = $regKey.GetValue($valueName)
$regKey.SetValue($valueName,$DefaultUserName)


The last step simply echoes something back so the user can see what happened. In VBScript

Wscript.echo "Changed DefaultUserName from '" & strValue & "' to '" & sNewUserName & "'"

and in PowerShell

Write-Host "Changed DefaultUserName from $oldValue to $DefaultUserName on $MachineName"


Full Source Code



The VBScript (without introductory comments) is 43 lines:

Dim sComputer
Dim sNewUserName

if WScript.arguments.count > 0 then
sComputer = WScript.arguments(0)
if WScript.arguments.count > 1 then
sNewUserName = WScript.arguments(1)
else
'default the user name to .
sNewUserName = ""
end if
else
'default computer name to local
sComputer = "."
sNewUserName = ""
end if

Dim sNewValue
sNewValue = ""

'constants for registry access
const HKEY_CURRENT_USER = &H80000001
const HKEY_LOCAL_MACHINE = &H80000002

'standard output
Set StdOut = WScript.StdOut

'get the registry object
Set oReg=GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _
sComputer & "\root\default:StdRegProv")

'set the key to grab
strKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
strValueName = "DefaultUserName"

'get the value
oReg.GetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, strValue

'set the new value
oReg.SetStringValue HKEY_LOCAL_MACHINE, strKeyPath, strValueName, sNewUserName

'display it
Wscript.echo "Changed DefaultUserName from '" & strValue & "' to '" & sNewUserName & "'"


The PowerShell version is 14 lines:

param ([string]$MachineName, [string]$DefaultUserName)
if ($MachineName -eq "") {$MachineName = $env:COMPUTERNAME}
if ($DefaultUserName -eq "") {$DefaultUserName = ''}

$keyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
$valueName = "DefaultUserName"

$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $MachineName)
$regKey = $reg.OpenSubKey($keyPath,$true)

$oldValue = $regKey.GetValue($valueName)
$regKey.SetValue($valueName,$DefaultUserName)

Write-Host "Changed DefaultUserName from $oldValue to $DefaultUserName on $MachineName"

Wednesday, April 2, 2008

Windows PowerShell

Started spending some time with Windows PowerShell, an next generation command shell and scripting environment for Windows. It can be downloaded from Microsoft's site. It's essentially finally an update to the classic DOS commands, providing simple commands for interacting with WMI, the file system, the registry, etc. The syntax is closer to .Net, and return values are actually rich .net data types.

After PowerShell is installed, you get a new 'console' (type 'powershell' in the run box to access) The syntax of PowerShell commands is verb-noun and has a fairly intuitive layout, I think this contrasts nicely from the DOS commands with vestiges of the 8.3 file names. In PowerShell, the following two commands will likely be the first you run:
    • get-command (lists all available commands)
    • get-help {command name} (lists further details on the given command)

DOS vs. VB Script vs. PowerShell
If DOS commands are the 1.0 version of an admin's 'console', powershell could be considered 2.0, and perhaps VBScript (or, yes, any WSH compatible scripting language) would be 1.5. While DOS relied on imperative commands, the admin using it didn't really have to 'think' like a programmer. To do any work with the file system or registry, without having a very obscure DOS batch file, VBScript had to be used. Also to do any work with WMI, VBScript had to be used. However to use VBScript, the admin really did have to think like a programmer, with looping, conditionals, object instantiation and disposal, etc. being for many tasks, critical for use. I've introduced a number of non-programmers to VBScript (WMI, etc.), and the barrier is two-fold, they have to learn the steps to accomplish the task at hand (i.e. retrieving, or setting something through WMI), as well as learn a modest amount of programming concepts they may have never dealt with. Sometimes that is a reason to stick with a DOS batch file to accomplish a step of the task, such as copying a file using 'xcopy' vs. using VBScript where the process may be more elegant and allow for more structure such as error handling (albeit in a rudimentary form)

PowerShell moves back to the DOS batch file type usage scenario, where an admin (non-programmer) can focus on the task at hand, with modest understanding of programming concepts , such as looping , conditionals and variables, and nearly no necessity of understanding objects lifetime concepts. In PowerShell, most commands return and accept objects as simply as they would accept string values.

The first advantage of PowerShell that struck me was the ease of interacting with WMI, even on a remote computers. Note that PowerShell is not a 'replacement' for WMI, any more that VBScript/WSH was WMI. WMI is still the underlying data structure to expose information about a computer. The syntax and library around it is what's changing here. The implicit functions (called cmdlets - "command-lets") provide much of it, and the adoption of a .net style syntax, where basically more stuff can be done on a single line of code helps

For example, this script I had sitting around to determine how much memory was on a remote computer
In VBScript:

strComputer = "ComputerB"
Set wbemServices = GetObject("winmgmts:\\"
& strComputer)
Set wbemObjectSet =
wbemServices.InstancesOf("Win32_LogicalMemoryConfiguration")

For Each wbemObject In wbemObjectSet
WScript.Echo "Total Physical Memory (kb): " &
wbemObject.TotalPhysicalMemory

Next


In PowerShell:
Get-WmiObject Win32_LogicalMemoryConfiguration –computername ComputerB

Albeit, I'm not a fan of terse-ness for terse-ness sakes (or else we would just write everything in Perl :)), but the ability to have a single command do what the most obvious intent, and having the ability to send that output into other commands.

Platforms
PowerShell is support in Windows XP or newer. However, at this point, PowerShell does not ship natively with any Microsoft OS, although it appears it will be an optional install on Windows Server 2008. Since it's not native on my client computers, I anticipate it primarily being used in admin 'push'-style scripts vs. something end-user facing.

Projection
At a very early glance, I'm seeing PowerShell replace the utility of DOS batch files and VBScript/WSH solutions as soon as the platforms ubiquitously support it. Right now, the barrier is computer running Windows 2000 or earlier (with no support for PowerShell), and the fact that even Windows XP computer already have support for DOS batch files and have WSH, while they don't have PowerShell.

For the short to mid-term, there will still be a number of DOS batch files and VBScript solutions for end-user facing needs: Functions that can run without admin privileges, ran by the user on demand, or during their log on or similar event. this will be simply because they most likely already have everything they need to run it.

However, for admin-driven scripts, where you as an IT person will be pushing something from your computer 'out' to other computers, or querying other computers, install PowerShell and try it out, starting with the replacements for some of the basic DOS commands you might now, and evolving into WMI and replacing your VBScript, if you have any. I think you will find it a productive environment to accomplish many common admin tasks.

In a future article, I will detail a line-by-line conversion from one of my more complex VBScripts into PowerShell, and also evaluate some of the PowerShell editors that provide some advanced edit/view features.

Further Reading
Free Windows PowerShell workbook: server administration
Windows PowerShell Getting Started Guide