Friday, May 29, 2009

WSUS 3.0 SP2 Upgrade from SP1

I ran an in-place upgrade of my WSUS 3.0 server from SP1 to SP2 RC.  It went well, but after the upgrade finished, it fired up the setup wizard and when it got to the step where it tries to initiate a connection to Microsoft, it just ran and ran and ran and wasn’t going anywhere.

So a quick check showed that the Update Services service was stopped, even though it was marked as Automatic (but no auto-restart).  Once I started the service again, the wizard continued on just fine and everything is working fine.  This led me to the following solution:  Wrap the upgrade inside a script to handle it all.

Dim objShell, objWmiService, colItems, objItem, r

Set objShell = CreateObject("Wscript.Shell")
' refer to the release notes for options DO NOT accept these blindly!
r = objShell.Run(1, "cmd /c WSUSSetup.exe /g /q DEFAULT_WEBSITE=1", True)
Wscript.Echo "Upgrade has been initiated..."
' check if the WSUSService service is running, force a start if not
query = "SELECT * FROM Win32_Service WHERE Name='WSUSService'"
Set objWMIService = GetObject("winmgmts:\\.\root\CIMV2")
Set colItems = objWMIService.ExecQuery(query,,48)
For Each objItem in colItems
If objItem.State <> "Running" Then
r = objShell.Run(1, "cmd /c sc start WSUSService", True)
End If
Next


This is just ONE way to do this of course.  There are many others.  I know that I could have used another WMI method invocation on the service to request the start action.  But I’ve found some issues with that which I cannot explain (at least not on Windows 7 or Windows Server 2008 machines), where the good old SC.exe command seems to do just fine.  It’s also fewer lines of code.

Thursday, May 28, 2009

Ini, Mini, Mighty Mo, I smell an INI file

Chris at DwarfSoft posted a very interesting chunk of code that defines a class for handling INI data structures.  His rationale for doing this, in lieu of using the Word.Application PrivateProfileString object is good.  It’s not a finished class, but it’s very good and I’ve added a few small tweaks to use on a project I’ve been working on.

' Adapted from post by Chris at DwarfSoft (link below)...
' http://www.dwarfsoft.com/blog/2009/02/27/ini-file-handler-for-vbscript/
'
' changes:
' added a GetKeys function to the class
' added a GetSections function to the class

Class IniFile
Private mIniFile
'
'----------------------- Sub Load ----------------------------

Public Sub Load(Filename)
LoadIni FileName,False
End Sub

'-------------------- Sub LoadIni ----------------------------

Public Sub LoadIni(Filename,JustDefaults)
Dim objFSO, objDictionary, objSubDictionary, file
Dim ini, arr, line, splitline, tmpsplit
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objDictionary = mIniFile
Set objDictionary = CreateObject("Scripting.Dictionary")
Set objSubDictionary = Nothing
Set mIniFile = objDictionary
If JustDefaults Then
Read = False
Else
Read = True
End If

If objFSO.FileExists(Filename) Then
Set file = objFSO.OpenTextFile(Filename)
ini = file.ReadAll()
file.Close
arr = Split(ini,vbCrLf)
For Each line in arr
If line = "##STARTDEFAULT" Then
Read = True
End If
If Read Then
If Left(Trim(Line),1) = "[" And Right(Trim(Line),1) = "]" Then
line = replace(replace(line,"[",""),"]","")
If Not IsEmpty(objDictionary.Item(line)) Then
Set objSubDictionary = objDictionary.Item(line)
Else
Set objSubDictionary = CreateObject("Scripting.Dictionary")
'objDictionary.Add line, objSubDictionary
Set objDictionary.Item(line) = objSubDictionary
End If
Else
If TypeName(objSubDictionary) = "Nothing" or _
IsEmpty(objSubDictionary) Then
Set objSubDictionary = CreateObject("Scripting.Dictionary")
objDictionary.Add "[]",objSubDictionary
Set objDictionary.Item("[]") = objSubDictionary
End If
If Left(Trim(line),1) = "#" Then
objSubDictionary.Item( "[" & _
objSubDictionary.Count & "]") = Trim(line)
Else
splitline = split(Trim(line),"=")
If TypeName(splitline) <> "Nothing" Then
If UBound(splitline) = 1 Then
tmpsplit = split(splitline(1), "#")
' Resolve a = error
If UBound(tmpsplit) >= 0 Then
objSubDictionary.Item(Trim(splitline(0))) = Trim(tmpsplit(0))
Else
objSubDictionary.Item(Trim(splitline(0))) = ""
End If
Else
'Error
End If
End If
End If
End If
If line = "##ENDDEFAULT" And JustDefaults Then
MsgBox "End Default"
Read = False
Exit Sub
End If
End If
Next
Else
Exit Sub
End If
End Sub

'----------------------- Function GetValue ----------------------------

Public Function GetValue(Section, Value)
Set objDictionary = mIniFile
If IsSection(Section) Then
Set objSubDictionary = objDictionary.Item(Section)
If IsEmpty(objSubDictionary.Item(Value)) Then
objSubDictionary.Remove(Value)
Else
GetValue = objSubDictionary.Item(Value)
End If
End If
End Function

'----------------------- Function IsSection ---------------------------

Public Function IsSection(Section)
Set objDictionary = mIniFile
If IsEmpty(objDictionary.Item(Section)) Then
objDictionary.Remove(Section)
IsSection = False
Else
IsSection = True
End If
End Function

'----------------------- Function GetSections ------------------------

Public Function GetSections()
Dim retval, item
Set objDictionary = mIniFile
If TypeName(objDictionary) <> "Nothing" Then
If Not IsEmpty(objDictionary.Keys) Then
For Each Key in objDictionary.Keys
If Key = "[]" Then
ElseIf Key = "" Then
Else
If retval <> "" Then
retval = retval & vbTab & Key
Else
retval = Key
End If
End If
Next
End If
End If
GetSections = retval
End Function

'---------------------- Function GetKeys -----------------------------

Public Function GetKeys(Section)
Dim retval : retval = ""
Set objDictionary = mIniFile
If IsSection(Section) Then
Set objSubDictionary = objDictionary.Item(Section)
For each Key in objSubDictionary.Keys
If retval <> "" Then
retval = retval & vbTab & Key
Else
retval = Key
End If
Next
End If
GetKeys = retval
End Function

'----------------------- Function IsSection ---------------------------

Public Function Save(FileName)
Set objDictionary = mIniFile
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set Outfile = objFSO.CreateTextFile(FileName)
If TypeName(objDictionary) <> "Nothing" Then
If Not IsEmpty(objDictionary.Keys) Then
For Each Key in objDictionary.Keys
If Key = "[]" Then
ElseIf Key = "" Then
Else
Outfile.WriteLine("[" & Key & "]")
End If
Set objSubDictionary = Nothing
If Not IsEmpty(objDictionary.Item(Key)) Then
Set objSubDictionary = objDictionary.Item(Key)
End If
If TypeName(objSubDictionary) <> "Nothing" Then
If Not IsEmpty(objSubDictionary.Keys) Then
For Each subKey in objSubDictionary.Keys
If Left(subKey,1) = "[" Then
OutFile.WriteLine(objSubDictionary.Item(subKey))
Else
OutFile.WriteLine(subKey & "=" & _
objSubDictionary.Item(subKey))
End If
Next
End If
End If
OutFile.WriteLine
Next
End If
End If
End Function

'--------------------- Function AddNextNumeric ------------------------

Public Function AddNextNumeric(Section,Value)
Set objDictionary = mIniFile
If IsEmpty(objDictionary.Item(Section)) Then
Set objSubDictionary = CreateObject("Scripting.Dictionary")
Set objDictionary.Item(Section) = objSubDictionary
objSubDictionary.Item("1") = Value
AddNextNumeric = 1
Else
Set objSubDictionary = objDictionary.Item(Section)
Number = 1
Do While IsEmpty(objSubDictionary.Item("" & Number))
Number = Number + 1
Loop
objSubDictionary.Item("" & Number) = Value
AddNextNumeric = Number
End If
End Function

'--------------------------- CONSTRUCTOR ------------------------------

Private Sub Class_Initialize
' Class Constructor
' Initialization goes here
Set mIniFile = Nothing
End Sub

'---------------------------- DESTRUCTOR ------------------------------

Private Sub Class_Terminate
' Class Destructor
Set mIniFile = Nothing
End Sub

End Class

That defines the class and class members. Now let's put it to work. Use use any standard INI file with sections identified within square brackets [ ] and keys with associated values beneath each section (KEYNAME=VALUE). Here's a basic example...

[SECTION_A]
Key1=ABC
Key2=DEF

[SECTION_B]
1=Red
2=Yellow
3=Green

And to put this contraption to work, here's a few tests to run it with...

Set ifile = New IniFile
ifile.Load("ini_file_test.ini")

v = ifile.GetValue("SECTION_A","CellMake")
Wscript.Echo "Value: " & v & vbCRLF

section = "SECTION_B"
keys = ifile.GetKeys(section)
For each key in Split(keys, vbTab)
v = ifile.GetValue(section, key)
Wscript.Echo section & "," & key & " = " & v
Next

Wscript.Echo

section = "SECTION_C"
keys = ifile.GetKeys(section)
Wscript.Echo section & " --> " & Replace(keys, vbTab, ",")

Wscript.Echo
Wscript.Echo "Sections..."
For each s in Split(ifile.GetSections(), vbTab)
Wscript.Echo s
Next

The rest of the class functions that need to be added are pretty common.  Things like updating section names, key names, values, and saving back to the file (which is there already).  This has actually renewed my interest in working with classes (I hate saying "Object Oriented" for some reason). If I drink enough coffee I might even get motivated to do something.

Shortcuts to Shortcuts

This is aimed at a fairly narrow niche, but here goes.

Let’s just say you have an intranet web site and you want to put a shortcut on each user’s desktop to the portal.  Easy enough.  Piece of cake.  No-Bake cake actually.  But let’s say you want to capture some information from each user hitting the web site.  Easy as well.  Piece of another cake.  Typically you would turn on integrated authentication and capture the HTTP_USER or REMOTE_USER session variables with IIS, or some equivalent with WebSphere or Apache or whatever you use.

But what if you wan to also collect the name of their computer?  The MAC address?  Whether they have Admin rights or not? 

You can do this with a lot of different scripting tools, but KiXtart provides a few more ready-made widgets to solve this task in less time.

The Ingredients:

  • A Windows-based network with Active Directory
  • Login script (KiXtart, in this case)
  • Liquefied Caffeine (hot or cold, doesn’t matter)

The Code:

Break ON
$scname = "Help Desk"
$url = "http://myintranet.company.com/helpdesk/?uid="+@userid+"&cn="+@wksta+
"&p="+@priv+"&m="+@address+"&d="+@date+"&t="+@time

$objShell = CreateObject("Wscript.Shell")
$path = ExpandEnvironmentVars("%USERPROFILE%")+"\Desktop\$scname.url"

? "creating/updating shortcut..."
$sc = $objShell.CreateShortcut($path)
$sc.FullName = $scname
$sc.TargetPath = $url
$sc.Save()
? "shortcut has been updated"


The time-saver here is the library of built-in "macros" available in KiXtart. These are basically global variables with special names, which expose information without having to paste in a bunch of additional code.



This is evident in the example above, by using the macros @userid (username), @wksta (computer name), @priv (“ADMIN” or ”USER”), @address (MAC), @date (current date in YYYY/MM/DD format), and @time (current time).



The code above will both create and replace the shortcut (if it already exists) each time the user logs on.  This automatically captures their login date and time in the URL of the shortcut.  Arguably a silly effort, but it’s just to show you CAN do this.  Rather than having to instruct users (and God help you!) on how to query their computer name, or installing another script or widget to do that with another mouse click, this takes care of that in one step.  As soon as the user hits the URL, your web page has the computer name, MAC, username, local permissions, date and time, and whatever.  You can also include @producttype (operating system name) @csd (version), @onwow64 (32 or 64 bit) and so on.



As I said, you can do this with VBScript, PowerShell, Perl, Python, and probably even with two sticks and bucket of mud and a case of beer.  But sometimes you need to pick the tool that does the job fastest and easiest.  That doesn’t mean it will be the best tool for all other needs.

Wednesday, May 27, 2009

Hands On. Eyes On. or Blind

Continuing on a little further regarding Troubleshooting, I realized right afterwards that there are different types of troubleshooting:

  • Hands On
  • Eyes On
  • Blind

Hands On – is pretty self-explanatory.  You get to put your physical hands directly on the machines involved with finding and fixing the problem.

Eyes On – is when you can’t put your hands directly on them, but you can either view the troubleshooting process over the shoulder of another person, or you get a decent amount of diagnostic information 2nd hand to investigate the problem.

Blind – this is when you try to help others over the phone or by chat.  You can’t see or touch the machines involved.  You can’t see diagnostic output.  You’re usually making educated guesses, at best, using limited information either spoken verbally or typed into a chat window.

Obviously, these are gradually degrading degrees of effectiveness.  Hands On is the ideal.  Eyes On is what you have to put up with a lot of the time.  Blind is what you try to invent stories to get away from.  “I hear my dog eating a small animal out back, gotta go!…”

Basic Troubleshooting 101

At 45 years, I’m even more amazed now than ever at just how incapable people are at performing basic troubleshooting.  Getting to the root cause of a problem.  Any problem.  While I’m focusing on “technology” for the moment, this applies to LIFE in general.  Every day I see people start to gather information about a problem, and before they finish listening to the whole story, are already diving in to “fix” the problem.

9 times out of 10 they end up not fixing the problem, but rather, they prolong or intensify it.  This drags the problem on longer.  The negative aspects of this are dangerous.  Wasted time is just the beginning.  If you’re a doctor, someone could die.  If you maintain certain kinds of machinery or systems, people could die.  This is serious shit!  If you’re laughing right now: shut the fuck up and pay attention!

Basic Troubleshooting

  1. Gather the facts (not the rhetoric, bullshit story, just the facts)
  2. Isolate the scope of the problem (how big, how far spread)
  3. Compare with something not-broken
  4. Look for patterns

Basic IT Troubleshooting

  1. Gather the facts
    1. Find out what changed
    2. Inspect event logs, file logs
    3. Eliminate the obvious (cables, power)
    4. When did the problem first occur
    5. Did it EVER work correctly
  2. Isolate the Scope
    1. Is it User-Specific (one user affected, others are not)
    2. Is it Machine-Specific (all users affected on same computer)
    3. Is it Application-specific (one app, or all apps)
    4. Is it device-specific (printer, scanner)
    5. Is it resource-specific (a particular shared folder)
  3. Compare
    1. Before and After log results
    2. Verify interfaces (ping, browse)
    3. Verify user accounts (enabled, locked, group memberships)
    4. Verify security settings
    5. What differs from this to another? (user, machine, app, device, etc)
  4. Look for Patterns
    1. Does it happen consistently
    2. Does it happen at particular days, hours, weeks
    3. Does it coincide with another process
    4. What circumstances cause it to occur

Nearly every time someone contacts me for help with something on their computer (and I’m talking about IT “professionals” here, not family, friends and so on), I ask “what do the event logs show?”, and I get the same answer “I haven’t checked yet.”

Here’s a real world example:  User calls in a support request saying their application is “broken and won’t launch anymore”.  Help Desk technician immediately uninstalls and reinstalls the application.  This process normally takes an hour per machine.  But guess what?  The problem returns.  Did they check to see if another user could run that application under their own login?  Did they check to see if the application works on other computers?

In one case, the problem was a license server not responding, so NONE of the applications on ANY computer were working.  Restarting a service fixed the problem.    In another case, the user’s profile had a corrupt registry key (HKCU) and simply deleting the registry key and subkeys forced the application to rebuild the keys and everything worked fine.  In both cases, the “fix” was completely wrong and wasted an hour of time for everyone and accomplished nothing.  Rushing in to fix a problem without being careful to diagnose it first is dangerous. It’s how space shuttles blow up.  It’s how ships run aground.  It’s how patients die.  We all make mistakes, but a mistake is a deviation from a normal pattern of NOT making mistakes.  If you make mistakes all the time, everytime, you need to find another career.

Finally, if you step back and look at your environment and find that you’re fixing the same kinds of problems a lot, that is almost always a clear indication that there wasn’t enough testing performed early on.  Whether it was picking the wrong products or technologies, or not implementing properly, or not training users to use it effectively, or not telling users it was coming (and when), well, somebody screwed up.

Conclusion

Slow down, at least a little, and be sure to gather everything you can about a problem so you can get your mind around it and solve it effectively.  The time you spend up front will usually save twice that on the other end when you try to solve the problem.

Tuesday, May 26, 2009

My Trip to City Hall

Today, after work, I accompanied my oldest daughter and one of her friends to a city council meeting. Her class is required to log a certain number of hours of attendance at city government functions, and these are pretty easy to get to. It was interesting, especially since I hold a fraction of a fraction of 1 percent respect for government. Most of what I see doesn't impress me, but I humored the kids and went along.

A few tips for anyone working with those fine folks of the Virginia Beach City Council:

  1. Oil that damn door! The entrance door to the room has a squeak like a haunted house. And to make things worse, I timed it and each time someone entered, it takes 4 seconds for the door to finally, and loudly shut. Sqqqueeeeeeeeeeeeeak!  Thump!
  2. Mr. Jones, the guy with the Ministerial hair cut: Drink a Red Bull before you read to the microphone. This guy talks so monotonous, and lackluster, that I thought I would have to stab my eyes out with a belt buckle just to stay awake.
  3. Put some signs up around that wonderful "campus" that help guide newcomers to the council meeting place. We have a "municipal center" that was designed by M.C. Escher.
  4. Instead of robbing school board funds to widen roads like Laskin, how about actually knocking out one of those projects you've spent money on and NEVER even tried to finish (let alone get started on). I can name a few: Nimmo Parkway, the Town Center walk-over, lightrail (yes, fricking lightrail, morons), the Rosemont/264/Bonney/Death-trap intersection, and so SOMETHING with that weed-pit where HQ and FX used to be.
  5. Add some sound effects with desk-top buttons.  It might liven things up a bit when a council member rebuffs a speaker.  Also, install a decibel sensing device that monitors the voice level of council speakers.  If they don’t maintain a minimum threshold, it should send an electric shock to their seat bottom to keep them sounding interested and engaged.

And I wonder how I could have such little faith in them spending our money right. Whatever.

Monday, May 25, 2009

Efficiency at the Cost of Freedom?

This is indeed a mindless rant.

When you get used to something and then it's no longer around, it can be a bit irritating to deal with. That's as true for programming as anything else in life. I bounce between several scripting languages, but mostly KiXtart, VBScript and PowerShell (as if you couldn't tell from my mindless rants already). But prior to this I worked primarily with LISP (AutoLISP and some CommonLISP or Franz LISP, no not Franz Liszt). One thing I run into quite often is the rather limiting single-expression nature of SELECT CASE or SWITCH CASE or SWITCH {} or whatever. I was used to the (COND) statement in LISP, and it was good.

Most people that have never used (cond) look at the code examples I give them and smirk to each other and say something like "so?". But imagine if PowerShell suddenly removed the "default" option from the Switch{} statement? Sure, you could code around that, but you'd probably ask yourself (or some other helpless victim at the coffee mess) "what happened to it? my precious?  why did they take it away?" at least once, anyway.
Here's an example of PowerShell vs LISP switch-case condition branching:
switch ($a) {
1 {action...}
2 {action...}
"a" {action...}
default {action...}
}




I don’t mean to pick on PowerShell.  It’s just the example I chose from a hat (ok, proverbially speaking, my hat is the size of my head, which isn’t big enough to hold more than one example).  The variable $a is the driving (determinant) variable which is being evaluated.   VBScript takes a similar approach…


Select Case a
Case 1: action...
Case 2: action...
Case "a": action...
Else: action...
End Select


It’s funny (ok, to me anyway) that some uses of VBScript look a lot like COBOL.  Wordy wordy wordy.


LISP doesn’t take that approach.  It lets you invoke a variable, or an expression, anywhere in the conditional branching.  This can save lines of code in many situations, and comes in handy for other things...




(cond
( (= a 1) (action...) )
( (expression) (action...) )
( (> (* a 500) 1000) (action...) )
( (= (setq x (expression)) "ABC") (action...) )
( T (action...) )
)





Metaphorically, or euphorically (coffee kicking in), having another capability opens up a world of potential for new directions and approaches to solving problems.  By the way, other useful features of LISP include statements like (mapcar), (apply) and my favorite of favorites (lambda). Of course, you can perform Lambda expressions in many languages. But sprinkling more LISPness on it you can easily code for conditional lambda definitions. You can also code for dynamic functions and dynamic recursion. Since most of the work I did "back then" was with respect to drawing geometry on screen, these kinds of capabilities really came in handy. Interpolating intersecting ellipse shapes and defining specific pick-points to perform explicit edit operations, all are much easier to do with these dynamic capabilities than with stronger-typed languages like VBA, VSA/VB.NET or even C#.



It seems that the dynamic, fluid, free-form aspect of languages are giving way to more strongly typed, rigidly defined languages like the .NET family and even Ruby, Python, etc. Seems odd that we wait until 2009 to get all clean-shaven in wrapped in a suit with our programming, when software and hardware advancements have all but eliminated the performance overhead "drag" we worried so much about in the 1990's. Sure, I know that there are situations where that gap is noticeable, but I've seen some insane number crunching in my time, and the relativity factor is almost laughable when it comes to desktop computing. There were "problems" that in the 90's required parallel Cray boxes to crunch for days, even weeks, to achieve a result. That same problem category can be solved on a standard HP/Dell desktop now for under a thousand dollars (USD). The performance overhead gap has shrunk to miniscule scale. So, rather than focusing on processor efficiency, why don't we instead focus on the human nature of coding? Get rid of strong typing. Leave that for compiled languages.



Even if it's not practical to remove the pre-compile definition of variables, which I think is absurd, why can't we at least make the compiler/runtime smart enough to read variable names to interpret symbol types? iSomething could easily guide the JIT or compiler to define as "Integer".  sSomething = string/char type.  dSomething = date value, and so on.  Save a line of code here and there. And get rid of DIM and SET and GET. Why we have GET and SET for the same objects and classes in 2009 is beyond me.


In 1995 we were writing C++ class templates in college, which overloaded functions based on a return value. It's not that difficult. So you could GET or SET with the same line of code, but the one that captured a return value into a variable was the one that invoked the GET member of the class.  Why are we still having to explicitly tell the compiler/runtime what we’re doing?  What we need to include or import?  This just seems like we’re going backwards.  Is it really 2009? It seems more like 1999.

I told you it was a mindless rant. I had too much coffee and someone let me near a keyboard. That was a mistake. :)

Automation without Documentation is Simply Dumb

Anytime I'm asked what I like most about working with "technology" (in my case: software technology), I always respond: "automation". I love to use software to make things work without my having to make them work each time they "run".

Each time we hire new employees, the HR information entered should automatically initiate processes to provision their user accounts, put their account into the appropriate groups, assign an email address, and so on. It should also drive what software "package" gets installed on their new computer.

Things like that. Scripts, utilities, group policy objects, whatever. I have no preferences. I do like to narrow down the best tool for a task by reviewing what needs to be accomplished. I've also run head-on into conflict with other IT groups in the past over this. They usually hear of something being automated and immediately feel threatened, which is dumb, but it is human nature.

So, automation is, or can be, a good thing. Right? Well, actually ONLY if you make the effort to document everything about the automation. Who, What, Where, When, Why and of course: How. These things need to be documented carefully and clearly. Not just for others, but for yourself as well. I've been in situations where I had automated some process, and it had been running quietly and reliably for years, and then needed to make a change. Sometimes it was an "uh oh?" moment as I tried to remember what a particular step was being done for, or where, or how. What was that service account used for? (another reason I prefer machine accounts over service accounts).

What I began doing was creating a folder structure for each automated process. In that folder collection I would store the scripts and utilities being used, as well as documentation files to describe what was going on and how it worked. That way I could easily go back and refresh my memory when I was asked about how something would impact one of my processes, or how one of my processes might impact something else. There was always the proverbial "what if you got hit by a bus?" concern also, which is entirely reasonable. So, I just wanted to say something on this topic. So remember kids: Automation without Documentation is like sex without a partner.

Friday, May 22, 2009

What is a Scripting Language?

That's a good question.  Far more knowledgable people than I have pondered this question.  It shouldn't surprise anyone that I'm not exactly a "pundit" on this (or pretty much any) subject.  But  I have spent the past 25 years in and around writing code of various ilks.  In college I spent plenty of time with compiled and interpreted languages.  The longest dance I've had to date was with LISP and it was a good time.  I'd like to think I've learned something along the way.

Back in college, the definition of "scripting" in contrast to "compiled" applications was sketchy, but the general idea was pretty clear to us.  It seemed that the differentiation fell into at least the following general guidelines:
  • Scripts were loosely (or un-) typed, while compilers were strongly typed
  • Scripts were designed for quick development and quick results
  • Compiled languages were designed for process efficiency and feature packaging
  • Scripts were aimed at part-time developers, like Sys-Admins
  • Compiled apps were aimed at programmers and software engineers
  • Scripts were interpreted at runtime, or semi-compiled (i.e. p-code, etc.)
But times have changed.  Scripts now bear many of the features (and burdens) of compiled languages.  The focus of many has been on the side of software programming, rather than system administration.  Many still exist that strike a good balance between complexity and flexibility.  But the definition itself, if there ever really was one, seems to be shifting gradually.  The bullet list above would have fit well in the 1990's, but now I'd bet some would argue over some of the specifics.  Maybe I'm holding on to old ideals too long.

If I had the time (and desire) to build my own scripting language, it would fit the following rules:
  • The "engine" would be self-contained (no installation required: KiXtart)
  • It would provide all of the standard functions for strings, math, dates, file systems, data source providers, and so on.  For example: I shouldn't have to write code to calculate date differences.
  • It would support objects and class templates, but not require them
  • It would support try/catch/finally (e.g. PowerShell v2)
  • It would provide input and output UI tools (e.g. VBScript: InputBox, MsgBox)
  • It would provide exposure of environment objects (e.g. KiXtart macros)
  • It would support robust string manipulation (e.g. PHP, Perl)
  • It would support list structures and structs (LISP, Scheme)
  • It would support obfuscation and tokenization (KiXtart, LISP, .NET)
  • It would rely on a consistent syntax structure (PowerShell)
  • It would support pre-compile library fetching without explicit inclusion (detect API invocation statements and automatically include references)
  • It would natively support dynamic recursion (LISP)
  • It would support in-line expression branching (LISP: (cond) )
  • It would support lambda and matrix expressions (LISP)
  • It would scratch my back before I knew it itched (ok, just kidding)
That's a tall order.  Maybe when I retire and the kids are grown and moved out, I can spend my golden years slumped over the keyboard and make this happen.  Oh wait... I forgot: I can't retire.  I can't afford to.  Well, like the Joe character from Joe's Garage (Frank Zappa), I can dream my imaginary perfect scripting language, that probably no one else in the world would use besides myself.

Thursday, May 21, 2009

Crank 'em Up, and Rip 'em Out (Selectively)

My previous post was kind of broad.  Somewhat like duck hunting with a shotgun. Swatting flies with a boat paddle.  Hauling trash with a dragster.  Well, you get the idea.  This post takes that previous pile of code and tweaks it a little to allow you to get fancy-shmancy with the input file.  Rather than just specify a name, this will make it possible to remove applications by Name, Caption, Description, Vendor, PackageCache, and so on.

'****************************************************************
' Filename..: app_uninstall.vbs
' Author....: skatterbrainz.blogspot.com
' Date......: 05/21/2009
' Purpose...: uninstall applications using an input file listing
'****************************************************************
Option Explicit

Const inputFile = "app_uninstall.txt"
Const strComputer = "."
Const RunVerbose = True ' False = run quietly, no output
Const TestMode = True ' False = enable it to run
Const ForReading = 1
Const ForWriting = 2

Dim objWMIService, objFSO, wmistring

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Sub Echo(s)
If RunVerbose = True Then
wscript.echo Now & vbTab & s
End If
End Sub

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Sub Uninstall(query)
Dim colItems, objItem, retval
On Error Resume Next
Set colItems = objWMIService.ExecQuery(query,,48)
If err.Number <> 0 Then
Echo "info: wmi query failure"
Exit Sub
ElseIf IsNull(colItems) Then
Echo "info: application not found"
Exit Sub
End If

For Each objItem in colItems
If TestMode <> True Then
Echo "info: requesting removal of application..."
retval = objItem.Uninstall()
If retval = 0 Then
Echo "info: uninstall was successful"
Else
Echo "error: uninstall request failed!"
End If
Else
Echo "test: caption = " & objItem.Caption
End If
Next
End Sub

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Sub Main()
Dim objFile, appName, pathname, shortname, fullname, inputpath
Dim appData, wmiField, wmiValue, strQuery, strLine

fullname = Wscript.ScriptFullName
shortname = Wscript.ScriptName
pathname = Replace(fullname, shortname, "")
inputPath = pathname & inputfile

Set objFSO = Wscript.CreateObject("Scripting.FileSystemObject")

wmistring = "winmgmts:{impersonationLevel=impersonate}!\\" & _
strComputer & "\root\cimv2"
On Error Resume Next
Set objWMIService = GetObject(wmistring)
If err.Number <> 0 Then
Wscript.Echo "error: " & err.Number & " = " & err.Description
err.Clear
Wscript.Quit(1)
End If

Echo "info: searching for input file..."
If objFSO.FileExists(inputPath) Then
Echo "info: loading applications list..."
Set objFile = objFSO.OpenTextFile(inputPath, ForReading)
Echo "info: applications list loaded successfully"
Do While objFile.AtEndOfStream <> True
strLine = Trim(objFile.ReadLine)
If Left(strLine,1) <> ";" And strLine <> "" Then
If InStr(1, strLine, "=") > 0 Then
appData = Split(strLine, "=")
wmiField = Trim(appData(0)) 'name of field to match
wmiValue = Trim(appData(1)) 'value to match against
Echo "info: wmiField [" & wmiField & "] wmiValue [" & wmiValue & "]"
strQuery = WMIQuery(wmiField, wmiValue)
Uninstall strQuery
End If
End If
Loop
objFile.Close
Set objFile = Nothing
Echo "info: processing complete"
Else
Echo "error: input file not found = " & inputPath
End If
Set objFSO = Nothing
End Sub

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Function wmiQuery(strField, strValue)
Dim retval : retval = ""
Dim tmp : tmp = "SELECT * FROM Win32_Product WHERE XFIELD='XVALUE'"
Const fieldList = "Caption Description IdentifyingNumber InstallLocation Name PackageCache Vendor"
If InStr(1, Lcase(fieldList), Lcase(strField)) <> 0 Then
retval = Replace(Replace(tmp, "XFIELD", strField), "XVALUE", strValue)
Else
Echo "error: invalid wmi class fieldname requested: " & strField
End If
wmiQuery = retval
End Function

'----------------------------------------------------------------
' comment: call the code to do the dirty work
'----------------------------------------------------------------
Main()


And below is an updated example of the input .TXT file. It borrows from the standard .INI format where you have keynames on the left and a value on the right (separated by an equal sign).



TEXT FILE EXAMPLE:

; application removal list
; WMI-FieldName=Value to match
; lines which begin with semi-colon are ignored
; blanks are ignored
Name=Autodesk DirectConnect 2.0
Name=MSXML 4.0 SP2 (KB954430)
PackageCache=C:\WINDOWS\Installer\b564880.msi


You can expand upon this in many ways. As I said before, and probably too-often: this is simply ONE way to skin this kitty. If you have a better way, that's great too. Drop a feedback comment if you have something to share or a question to ask. Thanks!

Crank 'em Up and Rip 'em Out

Ever wanted to batch remove software from computers on your network, but maybe you don't have SMS, or Altiris, or System Center, or OpenView, or Tivoli, or a band of Mexican gunslinging marauders with thumbdrives in their bandalaros?  Maybe you're dying for AppLocker in Windows 7 and Windows Server 2008 R2, but unfortunately: they're not released yet.

There's quite a few ways to do this.  One way which I have used in the past, which is also one of the laziest ways, is to use a computer start-up script and Group Policy.  Probably THE laziest way is to order one of your staff to walk around and do this manually.  But maybe on the bright side it would promote the perception of higher-quality customer service.  "Look, here comes that dorky IT guy.  I get to punch him first!"

Basically, you create a script and put it in a shared folder.  You grant permissions to the shared folder (NTFS and Share level) so the "Domain Computers" group will have READ access.  Then you create a Group Policy Object (GPO) with the requisite start-up script setting to point to the script in the shared folder path (always use the UNC path).  You can use almost any script tool.  I've used VBScript, BAT and KiXtart mostly, but you could use Perl, PowerShell or whatever works.  As always: TEST CAREFULLY on a FEW machines before you pull the pin and run screaming away.

This version is VBScript and uses an accompanying .TXT file with the same name.  The TXT file should reside in the same shared folder as the .VBS script file.  The code is actually a little more compact with KiXtart, but I haven't bothered porting it to any other languages so I don't know what that might look like.  Instead of "CSCRIPT /NOLOGO <PATH>\app_Uninstall.vbs" in the GPO setting, you can put that line into a .BAT file and have the GPO call the .BAT file instead.  That opens up other possibilities as well, but I'll shut up and dump the code here...

'****************************************************************
' Filename..: app_uninstall.vbs
' Author....: skatterbrainz.blogspot.com
' Date......: 02/11/2008
' Purpose...: uninstall applications using an input file listing
'****************************************************************
Option Explicit

Const inputFile = "app_remove_list.txt"
Const strComputer = "."
Const debugEnabled = True

Const ForReading = 1
Const ForWriting = 2

Dim objWMIService, objFSO, wmistring

wmistring = "winmgmts:{impersonationLevel=impersonate}!\\" & _
strComputer & "\root\cimv2"
Set objWMIService = GetObject(wmistring)

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Sub DebugPrint(s)
If debugEnabled = True Then
wscript.echo Now & vbTab & s
End If
End Sub

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Sub Uninstall(app)
Dim colSoftware, objSoftware, retval
DebugPrint "info: searching for " & Quote(app) & "..."
On Error Resume Next
Set colSoftware = objWMIService.ExecQuery("Select * from Win32_Product Where Name='" & app & "'")
If err.Number <> 0 Or (Lbound(colSoftware)=Ubound(colSoftware)) Or IsNull(colSoftware) Then
DebugPrint "info: " & Quote(app) & " is not installed"
Exit Sub
End If
DebugPrint "info: requesting removal of application..."
For Each objSoftware in colSoftware
retval = objSoftware.Uninstall()
If retval = 0 Then
DebugPrint "info: uninstall was successful"
Else
DebugPrint "error: uninstall request failed!"
End If
Next
End Sub

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Function Quote(s)
Quote = Chr(34) & s & Chr(34)
End Function

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Sub Main()
Dim objFile, appName, pathname, shortname, fullname, inputpath
fullname = Wscript.ScriptFullName
shortname = Wscript.ScriptName
pathname = Replace(fullname, shortname, "")
inputPath = pathname & inputfile
Set objFSO = Wscript.CreateObject("Scripting.FileSystemObject")
DebugPrint "info: searching for input file..."
If objFSO.FileExists(inputPath) Then
DebugPrint "info: loading applications list..."
Set objFile = objFSO.OpenTextFile(inputPath, ForReading)
DebugPrint "info: applications list loaded successfully"
Do While objFile.AtEndOfStream <> True
appName = Trim(objFile.ReadLine)
If appName <> "" And Left(appName, 1) <> ";" Then
Uninstall appName
End If
Loop
objFile.Close
Set objFile = Nothing
DebugPrint "info: processing complete"
Else
DebugPrint "error: input file not found = " & inputPath
End If
Set objFSO = Nothing
End Sub

'----------------------------------------------------------------
' comment:
'----------------------------------------------------------------

Main()


The .TXT file should contain the actual names of the applications as they would appear in the Add or Remove Programs list in Control Panel. You can fetch them from there, or from the registry by going under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall. Each application is listed as a sub-key and you would query the DisplayName value for each. There's lots of ways to do that, so I won't bother with that. But here's an example .TXT file to play with...



; Application Removal List
Stupid Application 2009
Farm Animals Calendar 2007
Visual Sewer Unclogger v10

Lines which are empty or begin with a semi-colon (;) are ignored by the script.  The script processing output (if DebugMode is set to True) would look like the following if the applications are not found on a given computer...

5/20/2009 8:43:40 PM	info: searching for input file...
5/20/2009 8:43:40 PM info: loading applications list...
5/20/2009 8:43:40 PM info: applications list loaded successfully
5/20/2009 8:43:40 PM info: searching for "Stupid Application 2009"...
5/20/2009 8:43:40 PM info: "Stupid Application 2009" is not installed
5/20/2009 8:43:40 PM info: searching for "Farm Animals Calendar 2007"...
5/20/2009 8:43:40 PM info: "Farm Animals Calendar 2007" is not installed
5/20/2009 8:43:40 PM info: searching for "Visual Sewer Unclogger v10"...
5/20/2009 8:43:40 PM info: "Visual Sewer Unclogger v10" is not installed
5/20/2009 8:43:40 PM info: processing complete


If you want to take this a step further (or farther?), you can capture the output to another text file and then push it up to another shared folder (remember to grant "Domain Computers" to allow CHANGE access, share and NTFS).  That way you can come in the next morning to look at who rebooted and what happened.  If you want to force it, kill the building power overnight and turn it back on.  Drastic, of course, but at least you'll know the computers had to be rebooted.

Tuesday, May 19, 2009

Why Break Your Own Registry, When You Can Break Someone Else's?

I've played around with creating, reading and deleting registry keys, subkeys and values on a local machine for years.  Nothing new to report that hasn't been reported to death, ad infinitum and ad nauseum for years.  Maybe even millennia?  So I got bored (a rarity in my busy life) and tinkered with remote registry destruction.  Ok, just kidding.  I do NOT condone or suggest destruction of anything beyond insects that invade your house or vehicle.  I had an ant infestation in my truck years ago, not fun.

While the last few posts have demonstrated an unsurprising similarity between VBScript, KiXtart and even PowerShell, this time around, each shows some interesting differences.  First up is the VBScript example:

Based on code from the following links:

http://www.activexperts.com/activmonitor/windowsmanagement/adminscripts/registry/
http://msdn.microsoft.com/en-us/library/aa393286(VS.85).aspx
http://msdn.microsoft.com/en-us/library/aa393297(VS.85).aspx

' VBScript Remote Registry
' Create keypath HKLM\SOFTWARE\ATestKey
' Create three subkeys, each with a different value

Const strComputer = "COMPUTERNAME"
Const strKeyPath = "SOFTWARE\ATestKey"
Const strValueName = "ValueName1"

' defined all just for convenience, only using HKLM
Const HKCR = &H80000000
Const HKCU = &H80000001
Const HKLM = &H80000002
Const HKU = &H80000003
Const HKCC = &H80000005
Const HKDD = &H80000006

Wscript.Echo "CONNECTING TO CLIENT: " & strComputer
Wscript.Echo
On Error Resume Next
Set oReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _
strComputer & "\root\default:StdRegProv")

If err.Number <> 0 Then
Wscript.Echo "unable to connect to " & strComputer
Wscript.Quit(1)
End If

Wscript.Echo "ADDING KEYS AND VALUES..."

test = oReg.CreateKey(HKLM, strKeyPath)
RegResult test

test = oReg.SetStringValue(HKLM, strKeyPath, strValueName, "ValueName1")
RegResult test

test = oReg.CreateKey(HKLM, strKeyPath & "\Subkey1")
RegResult test

test = oReg.CreateKey(HKLM, strKeyPath & "\Subkey2")
RegResult test

test = oReg.CreateKey(HKLM, strKeyPath & "\Subkey3")
RegResult test

test = oReg.SetExpandedStringValue (HKLM, strKeyPath & "\Subkey1", "ValueName1", "%PATH%")
RegResult test

iValues = Array("string1", "string2")
test = oReg.SetMultiStringValue(HKLM, strKeyPath & "\Subkey3", "ValueName2", iValues)
RegResult test

test = oReg.SetDWORDValue(HKLM, strKeyPath & "\Subkey2", "ValueName1", 8)
RegResult test

uBinary = Array(1,2,3,4,5,6,7,8)
test = oReg.SetBinaryValue(HKLM, strKeyPath & "\Subkey3", "ValueName1", uBinary)
RegResult test

Wscript.Echo "DELETING KEYS AND VALUES..."

test = oReg.DeleteValue(HKLM, strKeyPath, "ValueName1")
RegResult test
test = oReg.DeleteValue(HKLM, strKeyPath & "\Subkey1", "ValueName1")
RegResult test
test = oReg.DeleteValue(HKLM, strKeyPath & "\Subkey2", "ValueName1")
RegResult test
test = oReg.DeleteValue(HKLM, strKeyPath & "\Subkey3", "ValueName1")
RegResult test
test = oReg.DeleteValue(HKLM, strKeyPath & "\Subkey3", "ValueName2")
RegResult test
test = oReg.DeleteKey(HKLM, strKeyPath & "\Subkey1")
RegResult test
test = oReg.DeleteKey(HKLM, strKeyPath & "\Subkey2")
RegResult test
test = oReg.DeleteKey(HKLM, strKeyPath & "\Subkey3")
RegResult test
test = oReg.DeleteKey(HKLM, strKeyPath)
RegResult test

Sub RegResult(v)
Select Case v
Case 2:
Wscript.Echo "registry update failed: object not found"
Case 0:
Wscript.Echo "registry update successful"
Case Else:
Wscript.Echo "registry update failed (test = " & v & ")"
End Select
End Sub


I apologize for the laborious use of capturing the return value and checking it for results, but I wanted to show that you can at least rudimentary checking of what happens at each step.  For a exhaustive list of WBEMerror enumerations, which are what the oReg object returns into the "test" variable, click here.  Good luck translating the integer returns from VBScript into values shown.  It's also interesting (to me anyway) that some of the enumerations don't show a Hex value, such as wbemErrOutOfDiskSpace



If you really don't care about error trapping, you could compact the above code significantly, but I would recommend trapping at least the minimal shown above.



I will post some horrible-looking KiXtart and PowerShell examples later.  Maybe a part 2 and part 3?  Family becons me to the dinner table.

Monday, May 18, 2009

Stupid, Lazy Scripting

Are you talking to me?  A title like that usually refers to my approach to scripting.

Tired of typing or copying this everywhere?

Wscript.Echo "something stupid"

Try this...

Sub echo(s)
Wscript.Echo s
End Sub


Now you can cut your code down to just this...



echo "something less stupid!"


If you really get even lazy-er...



Sub z(s)
Wscript.Echo s
End Sub


z "something brief, but still stupid"

Finding Old Files = Old Stuff is New Again

There are a lot of scripts (and canned apps as well) floating about that will find files older than a given date or older than a given number of hours, days, weeks, months, years, decades, centuries, millenium.  Ok, seriously, you can't have any "real" electronic files older than a decade unit of measure.  Can you?

Some of the most succinct examples I've seen are those written by Don Hite.  If you haven't seen Don's site, you should (click here).  Here's one, and another.

Rather than try to "me-too!" this, I thought about dropping this into the blender to find out Will it Blend?  What I mean is: how does it look in different scripting languages.

Task: Identify files in a specified folder older than "x" days.  Either by Date-Created or Date-LastModified.  You have to be careful.  Sometimes, and I still don't know why, you can find situations where Windows reports that a file was Created on a date which is more recent than the LastModified date.  Maybe a time warp?  Who knows.  So I take the most-recent date for comparison.

Let's blend them, shall we?

VBScript Example

strFolder = "c:\temp"
intDaysOld = 60

Dim objFSO, objFolder, objFile, age, age1, age2, note
Dim iCount : iCount = 0
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder(strFolder)

Wscript.Echo "scanning " & strFolder
Wscript.Echo

For each objFile in objFolder.Files
age1 = DateDiff("d", objFile.DateLastModified, Now)
age2 = DateDiff("d", objFile.DateCreated, Now)
If age1 < age2 Then
age = age1
note = ""
ElseIf age2 < age1 Then
age = age2
note = "*"
Else
age = age1
note = ""
End If
If age > intDaysOld Then
Wscript.Echo objFile.Name & vbTab & _
objFile.DateCreated & vbTab & _
objFile.DateLastModified & vbTab & _
objFile.Size & vbTab & _
age & vbTab & note
iCount = iCount + 1
End If
Next

Set objFolder = Nothing
Set objFSO = Nothing

Wscript.Echo
Wscript.Echo iCount & " files found older than " & intDaysOld & " days old"


KiXtart Example



Break ON

$strFolder = "c:\temp"
$intDaysOld = 60

Dim $objFSO, $objFolder, $objFile, $age, $age1, $age2, $note
Dim $iCount, $Tab

$iCount = 0
$Tab = Chr(9)

$objFSO = CreateObject("Scripting.FileSystemObject")
$objFolder = $objFSO.GetFolder($strFolder)

? "scanning $strFolder" ?

For each $objFile in $objFolder.Files
$filename = $objFile.Name
$age1 = DaysOld($objFile.DateLastModified)
$age2 = DaysOld($objFile.DateCreated)
If $age1 < $age2
$age = $age1
$note = ""
Else
If $age2 < $age1
$age = $age2
$note = "*"
Else
$age = $age1
$note = ""
EndIf
EndIf
If $age > $intDaysOld
? $objFile.Name + $Tab +
$objFile.DateCreated + $Tab +
$objFile.DateLastModified + $Tab +
$objFile.Size + $Tab +
$age + $Tab + $note
$iCount = $iCount + 1
EndIf
Next

$objFolder = 0
$objFSO = 0

?
? "$iCount files found older than $intDaysOld days old"

;----------------------------------------------------------------
; comment: Calculate Days from DATE to Today
;----------------------------------------------------------------

Function DaysOld($date)
Dim $sc, $result
$sc = CreateObject("ScriptControl")
$sc.Language = "vbscript"
$result = $sc.Eval("DateDiff(" + Chr(34) + "d" + Chr(34) + ", " + Chr(34) + $date + Chr(34) + ", Now)")
$sc = 0
$DaysOld = $result
EndFunction


PowerShell v2 Example



param(
[string]$compare_method = "created"
)

$strFolder = "c:\temp"
$intDaysOld = 60
$tab = [char]9
$backdate = (Get-Date).AddDays($intDaysOld * -1)
$i = 0

# verify a valid compare-method argument value was requested
# default is "created" if none is specified

if (($compare_method -ieq "created") -or ($compare_method -ieq "modified")) {

# determine most-recent age of a given file object

function ShowInfo($file) {
if ($compare_method -eq "created") {
$d = new-timespan $(get-date $file.CreationTime) $(get-date)
if ($file.CreationTime.AddDays($intDaysOld * -1) -le $backdate) {
$file.Name $tab $file.CreationTime $tab $file.Length $tab $d.Days $tab "C"
}
}
else {
$d = new-timespan $(get-date $file.LastWriteTime) $(get-date)
if ($file.LastWriteTime.AddDays($intDaysOld * -1) -le $backdate) {
$file.Name $tab $file.LastWriteTime $tab $file.Length $tab $d.Days $tab "M"
}
}
}

#****************************************************************
# comment: filter files using datestamp age comparison
#****************************************************************

filter FileAge($days) { if (( $_.LastWriteTime -le $backdate -and $_.CreationTime -le $backdate )) { $_ }}

write-host "scanning $strFolder...`n"
if ($compare_method -eq "created") {
write-host "FileName $tab DateCreated $tab Size $tab Age $tab Note"
}
else {
write-host "FileName $tab DateModified $tab Size $tab Age $tab Note"
}
dir $strFolder | FileAge $intDaysOld | foreach-object { ShowInfo $_; $i++; }
write-host "`n$i files were found older than $intDaysOld days"
}
else {
write-host "`ninvalid parameter!"
write-host
write-host "usage: .\list-old-files.ps1 [compare-method]"
write-host
write-host " created - compare files by date-created"
write-host " modified - compare files by date last-modified"
write-host " created is the default (if not specified)"
write-host
}


### Some useful links...


http://powershell.com/cs/blogs/tips/archive/2008/11/20/finding-old-files.aspx


http://www.microsoft.com/technet/scriptcenter/topics/winsh/convert/dateadd.mspx


Code Talk



Most of the code you see here is adapted from what others post on the Internet.  I search and scavenge and assembly my own spin on things to suit (a) my needs and (b) my preferences for format, structure and documentation.  What you see here is about 75 percent discovery and 25 percent adaptation and reformatting.



The PowerShell example warrants a little explanation.  It's very different from the other two obviously.  I had to scrounge around to put that together, and I'm almost certain someone will look at it either laugh hysterically or throw-up all over their screen and keyboard.  Following up with something like "wow, what an idiot!  That can be done with [x] lines of code!"  So, don't blindly accept my work as the end-all solution to a problem.  It's simply meant to be an "example".  One example.  I urge you to explore your own solutions and improve upon anything I spew forth.



Ok, back to the code: First, I establish a date ($backdate) which is [60] days in the past from the current date.  Then I compare datestamps of files against that.  The function ShowInfo() compares the date stamp on the file object with the $backdate value to determine the age and then formats a line of data with tab separation.  The collection is first obtained from the Dir() function.  The output of which is piped directly into the FileAge filter expression to weed out files that do not exceed the specified age limit (60 days).  Then I allow the use of an input argument ($compare-method) to determine whether to calculate the file age by using the CreationTime property, or the LastWriteTime property.



Conclusion



Each of these can easily be ammended, appended, prepended, suspended and stipended (maybe up-ended) to do more useful things.  Maybe delete, rename or move the files it finds.  Whatever.  The point of this exercise is as always to just observe each language next to the other and see how they can each help.  If you work with one language all the time, it's like using the same hammer all the time.



You need to open your tool box and play with the other tools.  Wait, that didn't sound quite right, umm, whatever.  If you have a snippet posted that you'd like to share, drop a comment here and I'll gladly link you up.

Sunday, May 17, 2009

Don't Forget to Mop Up

I mentioned that over the weekend I swapped out a domain controller in a Windows Server 2008 domain.  It happened to be the ONLY domain controller, which, although not recommended, made things a little trickier.  In most respects everything went well.  But I found later on that clients were taking far longer to login than they used to.  Even Windows 7 clients, which normally login very fast (faster than XP or Vista in my opinion).  I began digging and within minutes found the source of the problem.

Step 1 - Event Logs
I opened the Event Viewer on one of the Windows 7 clients and dove into the System log.  Nothing there at all.  Then the Security log.  Nada.  Then the Application log.  Voila!
Log Name:      Application
Source: Group Policy Drive Maps
Date: 5/17/2009 8:55:57 AM
Event ID: 4098
Task Category: (2)
Level: Warning
Keywords: Classic
User: SYSTEM
Computer: Desktop4.acme.local
Description: The user 'V:' preference item in the 'Drive Mappings {3B6D54F2-E7DC-45A5-A66E-59C16CE2CB1A}' Group Policy object did not apply because it failed with error code '0x80070035 The network path was not found.' This error was suppressed.


There was an entry for each of the allocated drive mappings, as well as one for the printer mapping:


Log Name:      Application
Source: Group Policy Printers
Date: 5/17/2009 8:56:41 AM
Event ID: 4098
Task Category: (2)
Level: Warning
Keywords: Classic
User: SYSTEM
Computer: Desktop4.acme.local
Description: The user 'HP Deskjet F4200 series' preference item in the 'Drive Mappings {3B6D54F2-E7DC-45A5-A66E-59C16CE2CB1A}' Group Policy object did not apply because it failed with error code '0x80070709 The printer name is invalid.' This error was suppressed.


It was Group Policy Preferences which were tripping up, because the "network path" it mentions indeed changed.  The V: drive (among other drive mappings) specified in the GPP setting was pointing to a UNC path that no longer existed.  The UNC path was for the previous DC server name.  While I moved the data and shares over to the new DC and it worked fine, the GPP was not updated.  The net result was that each client was trying to find the share to reconnect the local drives and causing it to wait during logins.


The printer mapping failed to resolve because the client from which it was shared happened to have been reloaded with the same operating system but someone forgot to share out the printer again.  Once the printer was shared and listed in the directory, everything synched up fine.  But next up, I had to update the GPP settings for the drive mappings.


Step 2 - Updating Group Policy Preferences


I could have remoted into the domain controller to do this, but since I had already installed the RSAT for Windows 7, I simply opened Group Policy Management on the client:


Capture1


Right-click on the "Drive Mappings" GPO and select "Edit":


Capture2



Right-click on each of the drive mappings to the right, and select "Properties".  From there, I simply replaced the old server name component of the UNC path with the new server name (e.g. "Dragon"):



Capture3



Once I updated all of them, I simply closed the GPM console, opened a CMD console on the client and typed GPUPDATE and hit Enter.  After a few seconds the Group Policy settings were refreshed and I type NET USE to list all the drive mappings.  They were all present and accounted for.  To verify UNC resolution, I simply enter a drive letter to list a directory...



C:> V:  (press Enter)

V:> DIR  (press Enter)



All is good.



Conclusion



In the past, I would have used a login script to manage drive mappings, but there are some logistical issues with that method which are better served with GP Preferences.  It still amazes me that admins are still using scripts to map drives, printers, make registry changes, add or remove shortcuts, edit environment variables, modify INI files, add files and folders, and so on.



Don't get me wrong.  Being a script developer, I still insist scripts are the way to go for many tasks, but for these types of issues you're way better off using GP Preferences.  In this particular scenario, GP Preferences saved me at least a few hours of work and having the built-in integration with Event logging, it was much faster and easier to diagnose the problem and take the appropriate action to resolve it.

Friday, May 15, 2009

Brain-sucking Date-Mashing Code

So, you may have already looked at the code I posted that shows how to invoke a function or object from one scripting engine to another.  For example, using the ScriptControl object API to call upon the Cosine function in JScript from KiXtart, or call the @PRIV macro in KiXtart from VBScript.  Nothing Earth-shattering in that.  But this was an interesting spin.

KiXtart doesn't have a built-in counterpart to the VBScript DateDiff() function (among other related functions).  There are UDF code chunks out there which are very good and very reliable, but there's another way (did you guess yet?): You can invoke the DateDiff() function in VBscript from KiXtart.  The trick is in how you prepare the expression before asking the ScriptControl object to "evaluate" it and return a result.  This applies to DateAdd() and pretty much all of the other goodies WSH+VBScript/JScript has to offer.

So which of the following three (without cheating) do you think is the one that actually works?

Example 1

Function DaysOld($date)
Dim $sc, $result, $cmd
$cmd = "DateDiff('d', "+Chr(34)+$date+Chr(34)+", Now)"
$sc = CreateObject("ScriptControl")
$sc.Language = "vbscript"
$result = $sc.Eval($cmd)
$sc = 0
$DaysOld = $result
EndFunction




Example 2





Function DaysOld($date)
Dim $sc, $result, $cmd
$cmd = "DateDiff("+Chr(34)+"d"+Chr(34)+", "+Chr(34)+$date+Chr(34)+", Now)"
$sc = CreateObject("ScriptControl")
$sc.Language = "vbscript"
$result = $sc.Eval($cmd)
$sc = 0
$DaysOld = $result
EndFunction




Example 3





Function DaysOld($date)
Dim $sc, $result, $cmd
$cmd = "DateDiff(d, "+Chr(34)+$date+Chr(34)+", Now())"
$sc = CreateObject("ScriptControl")
$sc.Language = "vbscript"
$result = $sc.Eval($cmd)
$sc = 0
$DaysOld = $result
EndFunction




 



The answer?  Is further down below...



 



 



The correct answer is example 2.  It's weird that unless you wrap the date values in double-quotes, you get nothing back.  The NOW object doesn't require that however.  Each COM object you work with via the ScriptControl conduit behaves a little differently based on how the object's own API is written and how values are passed through the pipeline.  But hopefully you can see there's some value to this.



So, a more esoteric version of DateDiff() done with this approach might be something like this...





Function DateDiff($mode, $date1, $date2)
Dim $sc, $result, $cmd
$cmd = "DateDiff("+Chr(34)+$mode+Chr(34)+", "+Chr(34)+$date+Chr(34)+", "+Chr(34)+$date+Chr(34)+")"
$sc = CreateObject("ScriptControl")
$sc.Language = "vbscript"
$result = $sc.Eval($cmd)
$sc = 0
$DateDiff = $result
EndFunction

Thursday, May 14, 2009

Regression Aggression or Digression Transgression?

I have a theory:

Any new technology that seeks to improve upon, or supercede, any existing technology, should NEVER take any steps backwards.  In the worst-case scenario, it should match the existing technology, and then provide improvement from that point onward.

That theory blows chunks and doesn't hold water.

How often do we all see "new" or "new and improved" crap that, at least in some way, doesn't hold up to something already in existence?  Cars are a great example.  Sure, they get better gas mileage and have a bitchin stereo and fo-shizzle paint and detailing.  But the trade-offs are a-plenty.  Plastic has replaced metal.  Vinyl-ish surfaces replace what was once inhabited by Leather.  Wing windows are gone.  Overall body styles are bland and communist looking.  Not to digress too much farther, but let's face it: In the 1960's and 1970's, you could instantly name the car from a half-mile away speeding past.  I dare anyone to name 4 out of 5 cookie-cutter vehicles passing them on any given day, even within a few yards of you.

Sorry, I digressed again.  Where was I?  Oh yeah, NOT going backwards with new technology.

So, KiXtart began life sometime in the early-mid 1990's.  Then came Windows Scripting Host and Visual Basic(R) Scripting or "VBScript" sometime around 1996.  It added some new features not found in BAT/CMD script, and a few not found in KiXtart as well, such as DateDiff, DateAdd, and so on.  Some could argue it lacked features found in KiXtart, and I wouldn't argue with them either.  Now we have PowerShell, and you would think it should be a "best-of-breed" language.  In some respect it very much is.  Object oriented, pipelining, tracing, well-structured API and consistent syntax (for the most part).  But even with version 2.0 CTP it lacks some very esoteric features found in VBscript and KiXtart, among others.  I'm a bit confused. 

Examples?

$a = "abc,def,ghi,jkl"
$b = $a.Split(",")

Now, I assumed I could do the following, but I was wrong...

$c = $b.Join(",")

The "correct" way is as follows...

$c = [string]::Join(",", $b)

Other examples would be InputBox() and MsgBox(), which are heavily used in VBScript files these days.  You end up doing the duct-tape dance with a COM interface (yep, the ScriptControl object).  Here's my version of the two functions in case you care to play with them...

#check-out: http://www.microsoft.com/technet/scriptcenter/topics/winpsh/convert/inputbox.mspx

function InputBox($p, $t) {
$a = New-Object -ComObject MSScriptControl.ScriptControl
$a.Language = "VBScript"
$a.AddCode("Function getInput() getInput = InputBox(`"$p`",`"$t`") End Function" )
$b = $a.Eval("getInput")
$b
}

InputBox "Test msg" "Test Title"


And here's one for the MsgBox() function, which actually invokes the PopUp() function via the ScriptControl object.  I defined some standard enumerations to make things a little more familiar...



#check-out: http://www.microsoft.com/technet/scriptcenter/topics/winpsh/convert/msgbox.mspx

$vbOkOnly = 0
$vbOkCancel = 1
$vbAbortRetryIgnore = 2
$vbYesNoCancel = 3
$vbYesNo = 4
$vbRetryCancel = 5

$vbCritical = 16
$vbQuestion = 32
$vbExclamation = 48
$vbInformation = 64

$vbYes = 1
$vbCancel = 2
$vbAbort = 3
$vbRetry = 4
$vbIgnore = 5
$vbNo = 7

function MsgBox {
param(
[string]$p = "Message text",
[string]$t = "Caption Title",
[int]$delay = 0,
[int]$btn = 0
)
$a = new-object -comobject wscript.shell
$b = $a.popup($p, $delay, $t, $btn)
$b
}

MsgBox "Select an option" "MsgBox Test" 0 ($vbOkCancel+$vbQuestion)


My question is simply: why?



Especially with version 2.0 in the doorstep, wasn't more time taken to review what the predecessor's have and ensure those were not only rolled into the new version, but done so in a consistent manner?  I'm not a compiler builder or language architect.  I'm certainly not on the same level as Jeff Snover, Don Hite, Don Jones and the other wunderkind brains we all love to read about (and I do, trust me).  I'm just voicing an arguable trivial concern as a developer and professional nobody.



I should be able to invoke a messagebox or inputbox like this...



$x = get-input -prompt "" -caption "" -buttons 2 -style 32 ...



I'm sure I could spend a few hours building my own cmdlet to do this, but why should I?



I should be able to test if an object holds a numeric value like this:



$a -is [numeric]



rather than like this...



[reflection.assembly]::LoadWithPartialName("'Microsoft.VisualBasic")

$b = [Microsoft.VisualBasic.Information]::isnumeric($a)



Then again, maybe I'm being an ass and shooting from the hip with half-loaded gun (which I probably do more often than I admit).



Now, before you start blasting me with "Oh sure.  Easy enough for Mr. Blowhard here to bitch about something like this.  I'd like to see him build PowerShell from scratch like these guys did."  And that would be an absolutely valid comment.  I applaud the genius, the daring, the time, sweat and tears that has gone into dreaming, designing, building, testing, refining, and giving birth to this amazing thing.  Not to mention putting it out into the open world and sharing a conversation with developers everywhere about how it evolves and grows.  I can't take anything away from that, and I wouldn't want to.  I'm just making a comment because, well, I'm eating a sub and have time to kill and it's been on my mind today.  I've also been listening to Adam Carolla most of the day and it gets me in a spunky mood sometimes.  Anyhow, I'll shut up for a while.  Cheers.