Entries in Code (14)

Friday
Mar302012

PowerShell - Use an XML File for Script Settings and Logging

One of the guys I work with was looking to automate some software installation on new machines, sort of location-specific modifications to a firm-wide image, and wanted a way to have a script run steps, reboot and restart at the next step. He was effectively looking for a way to maintain information and state between times that the script ran. I thought using a local XML file to store states and log events into, treating it as a database that could remain on the machine afterwards for reference in troubleshooting might work well for him and created some code he could use.

I wound up building a few functions that would allow his script to create and interact with the local XML file so that he could both store and retrieve relevant values and log events along the way. This is broken down into functions that can be placed in a single script or that you can place in other files, whatever floats your boat.

Example of file

With the functions below you can:
  • Read and write status step value
  • Create, read and update configuration-params
  • Write only to log entries with some activities being auto logged
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<setup>
  <status step="3" />
  <configuration>
    <param type="script version">1.0</param>
    <param type="ip address">129.168.1.108</param>
  </configuration>
  <log>
    <entry timestamp="03/20/2012 14:33:34">Log created</entry>
    <entry timestamp="03/20/2012 14:33:34">W00t!</entry>
    <entry timestamp="03/20/2012 17:18:44">Config/Param update. Old: 129.168.1.17, New: 129.168.1.18</entry>
    <entry timestamp="03/20/2012 17:23:01">Error, attempted to update step in log file to a blank value.</entry>
    <entry timestamp="03/20/2012 17:41:10">Stage value update. Old: 2, New: 3</entry>
  </log>
</setup>

Main - Control flow from a single function

Example of setting the script up and how to call the functions;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
## Load Location-based Variables
##############################################################################
## Declare variables with :Script scope
##############################################################################
 
## Need Configured
    $Script:scriptversion = "1.0"
    $Script:logfilefolder = "C:\temp"   # will be created if does not exist
    $Script:logfilename = "C:\temp\MachineSetupLog.xml" # will be created if does not exist
 
## General Use
    $Script:currentstep = "Error"  # This variable will hold your current step counter if needed in IF/THEN
 
############################################################################## 
## Control - Main
##############################################################################
    Function Main{ 
     
        ## initialize log and get step value (in case of restart)
        PrepLogFile
        GetStepFromLog
         
        ## updates the step value in the log and the script variable $currentstep, both at once
        UpdateStepInLog("2")
         
        ## add arbitrary text to log as an entry, timestamp added by function
        #AddEntryToLog("W00t!")
         
        ## add param entry to log will overwrite if already exists, logging old and new values
        ##  format is: AddConfigurationEntry <param type> <param value> <log entry (1/0)>
        ##  The third parameter is a 0=false 1=true setting if the change should show old/new values in the log
        ##    set this to 0 when removing or changing a sensitive item such as a password.
        AddConfigurationEntry "ip address" "192.168.0.1" 1
         
        ## read parameters back: $foo = GetConfigurationEntry("IP Address")
        #PopUp(GetConfigurationEntry("ip address"))
        #PopUp(GetConfigurationEntry("script version"))
         
        ##Done
        PopUp("Main Complete")
    }

Popup Creator Function

For debug really, function for popping up an alert to notify of some value/state
1
2
3
4
5
6
7
8
9
##############################################################################
## function PopUp - pops a message to user
##  usage: PopUp(GetConfigurationEntry("ip address"))
##  usage: PopUp("More Coffee!")
##############################################################################
    Function PopUp([string]$messageIn){
        $a = new-object -comobject wscript.shell
        $b = $a.popup($messageIn,0,"CBH Script Alert",1)
    }

Exit Script Function

This was provided to call when an error state was detected or the need to shut the process down before the natural end occured, it logs the event and the message you pass to it to the log.
1
2
3
4
5
6
7
8
9
##############################################################################
## function ExitScript - Logs message and ends execution [ARG-STRING]
##  usage:  ExitScript("1 cannot = 2, aborted.")
##############################################################################
Function ExitScript($logmessage){
    $endingmessage = "Ending Execution with status: " + $logmessage
    AddEntryToLog($endingmessage)
    exit
}

Creates folder and log if they do not exist

Note the 2 string variables in the main function used here to specify the folder and file path. This also creates the blank XML template with a few values filled in. Mod as needed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
##############################################################################
## function PrepLogFile - Creates folder and log if they do not exist
##  usage: PrepLogFile
##  usage notes: be sure to set values of $Script:logfilefolder and $Script:logfilename beforehand
##############################################################################
Function PrepLogFile{
#Create folder if needed
    if (!(Test-Path -path $logfilefolder))
    {
        New-Item $logfilefolder -type directory
    }
    ## Create our temp file if needed (with content, elements on different lines for readability later)
    if(!(Test-Path $logfilename))
    {
        $date = Get-Date
        $blanklog = "<entry timestamp='" + $date + "'>Log created</entry>"
        $blankparam = "<param type='script version'>" + $scriptversion + "</param>"
 
        add-content $logfilename "<setup>"
        add-content $logfilename "<status step='0' />"
        add-content $logfilename "<configuration>"
        add-content $logfilename $blankparam   
        add-content $logfilename "</configuration>"
        add-content $logfilename "<log>"
        add-content $logfilename $blanklog
        add-content $logfilename "</log>"
        add-content $logfilename "</setup>"
        $logfilename.Save($logfilename)
    }
}

Pulls current step from log file

Calling this reads the value from your file and sets the script-level variable $Script:currentstep to the value of the step.
1
2
3
4
5
6
7
8
##############################################################################
## function GetStepFromLog - Pulls current step from log file into $Script:currentstep
##  usage: GetStepFromLog
##############################################################################
    Function GetStepFromLog{
        $xml = [xml] (Get-Content $logfilename)     #open file
        $Script:currentstep =  $xml.setup.status.GetAttribute("step")   #map currentstep to XML step value
    }

Updates the step value

This sets the step in your file and updates the script-level variable $Script:currentstep while it is at it. It also makes a timestamped log entry of this change, reflecting the old and new values.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
##############################################################################
## function UpdateStepInLog - Set step to new value log file [ARG-STRING]
##  usage: UpdateStepInLog("42")
##############################################################################
    Function UpdateStepInLog($newstep){
        if($newstep -ne "")
        {
            #Open xml file and change step to newstep value
            $xml = [xml] (Get-Content $logfilename)     #open file
            $status = $xml.setup.status     #map $status to that element in XML
            $status.step = $newstep     # set value
            $xml.Save($logfilename)     #save          
            GetStepFromLog      # Refresh $Script:currentstep from log we just changed
            $logmessage = "Stage value update. Old: " + $oldstep + ", New: " + $newstep
            AddEntryToLog($logmessage)
        }
        else
        {
            PopUp("Error, trying to update step in log file to a blank value")
            AddEntryToLog("Error, attempted to update step in log file to a blank value.")
        }
    }

Make log entry

This creates a timestamped entry in the log portion of the XML file for you. Use liberally.
1
2
3
4
5
6
7
8
9
10
11
12
13
##############################################################################
## function AddEntryToLog - Adds a new 'entry' node and datestamp to log file [ARG-STRING]
##  usage: AddEntryToLog("Look at me still logging when there's science to do.")
##############################################################################
    Function AddEntryToLog($message){
        $date = Get-Date    #for datestamp
        $xml = [xml] (Get-Content $logfilename)     #open file
        $newEntry = $xml.CreateElement("entry")     #create new entity 'entry'
        $newEntry.set_InnerXML($message)        #insert the message
        $newEntry.SetAttribute("timestamp",$date)   #populate timestamp attribute
        $xml.setup.log.AppendChild($newEntry)   #append to <log> entity
        $xml.Save($logfilename)     #save
    }

Add a configuration parameter

This will update the value if it exists already (and is a change) or add and entry if this is new. Additionally, it logs the old/new values when it updates an entry UNLESS you tell it to supress logging of the old, new values. This was added as the need to use these parameters for sensitive information might arise and though they may be purged at the end of the script, would remain in the log unless we allowed suppression.

If you call this with the log bit set to 0 it will supress logging the old/new value. If you pass a 1 it will log all details of your change. This is recommended.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
##############################################################################
## function AddConfigurationEntry - Adds a new 'parameter' node and configuration portion of log file [ARG-STRING], [ARG-BOOL]
##  usage (standard): AddConfigurationEntry "ip address" "129.168.1.1" 1
##  usage (supress): AddConfigurationEntry "secret value" "foobar!" 0
##############################################################################
    Function AddConfigurationEntry{
        param([String]$paramtype, [String]$paramtext, [Bool]$logchange) # map parameters to variables
            # the boolean value '$logchange' will suppress the log entry when set to false
        $xml = [xml] (Get-Content $logfilename)     #open file
         
        #already an item?
        $logatend = "no"        # flag to log after we close the file
        $logmessage = ""    # holder in case we need to log
        $test = $xml.setup.configuration.param | where { $_.type -eq $paramtype} #look for pre-existing param
        if($test)
        {
            # already exists
            # only update if they are not the same value
            if($test.InnerXML -ne $paramtext) #test for inequality
            {
                # prepare to log this change, storing the old value and the new one
                if($logchange) # only do this if $logchange function param is true
                {
                    $logmessage = "Config/Param update (" + $paramtype + "). Old: " + $test.InnerXML + ", New: " + $paramtext                          
                }
                else # generic message
                {
                    $logmessage = "Config/Param update (" + $paramtype + "). Values supressed."
                }
                $logatend = "yes"
                #set and save the change
                $test.InnerXML = $paramtext # set value
                $xml.Save($logfilename)     #save              
            }
        }
        else # OK to add
        {
            $newEntry = $xml.CreateElement("param")     #create new entity 'param'
            $newEntry.set_InnerXML($paramtext)      #insert the message
            $newEntry.SetAttribute("type", $paramtype#populate type attribute
            $xml.setup.configuration.AppendChild($newEntry) #append to <configuration> entity
            $xml.Save($logfilename)     #save      
        }
        if($logatend -eq "yes")
        {
            # logging was not working while the file was open, we moved it to the end here and it works.  boom.
            AddEntryToLog($logmessage)
        }
    }
Thursday
Nov032011

SharePoint Popup Contextual Images

Also known as the 'hover over for screenshot' functionality.  This is a nice jquery driven code snippet that will take any link on your SharePoint page that has an image as its url and allow the user to simply hover over the link to see the image.

This has come in handy for providing contextual help in the form of screenshots right in with content.  A dead simple implementation might look like the following with a hyperlink column in your list and that being the only element in the view.

Yielding this type of thing when you hover over a link on your page:

Otherwise this just requires a reference to your local jquery file (or maybe not local, do what you want cowboy).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script type="text/javascript" src="jquery.min.js"></script></span></div>
<script type="text/javascript"></span></div>
<br /></span></div>
function imagePreview(){</span></div>
arrOfImageTypes = ['jpg','jpeg','gif','png'];</span></div>
 $("table.ms-listviewtable td.ms-vb2 a").hover(function(e){</span></div>
 var href = this.href;</span></div>
 var img = href.substring(href.lastIndexOf('.')+1).toLowerCase();</span></div>
 if(href.indexOf('http')==0 && $.inArray(img,arrOfImageTypes)>-1){</span></div>
    $("body").append("<img id='preview' src='"+ this.href +"' alt='Image preview' />");</span></div>
 }</span></div>
 var obj = $("#preview");</span></div>
 var offset = $(this).offset();</span></div>
 var winHeight = $(window).height();</span></div>
 var winWidth = $(window).width();</span></div>
 var scrollLeft = $(window).scrollLeft();</span></div>
 var scrollTop = $(window).scrollTop();</span></div>
 var objHeight = obj.outerHeight();</span></div>
 var objWidth = obj.width()+15;</span></div>
 if(((winWidth+scrollLeft)-offset.left)<objWidth){</span></div>
 offset.left=((winWidth+scrollLeft)-objWidth);</span></div>
 }</span></div>
 var maxHeight = (winHeight+scrollTop)-offset.top;</span></div>
 if(objHeight>maxHeight){</span></div>
 if(offset.top-scrollTop>objHeight){</span></div>
 offset.top=offset.top-objHeight-20;</span></div>
 }</span></div>
 height = (objHeight<winHeight)?objHeight:winHeight;</span></div>
 }</span></div>
 obj.css({"position":"absolute","top":(offset.top+20)+"px","left":(offset.left+20),"border":"1px solid black"})</span></div>
 .fadeIn("fast");</span></div>
 },</span></div>
 function(){</span></div>
 $("#preview").remove();</span></div>
 });</span></div>
};</span></div>
<br /></span></div>
// Call the script on page load</span></div>
$(document).ready(function(){</span></div>
 imagePreview();</span></div>
});</span></div>
</script></span></div>
Tuesday
Oct112011

Google Stock Ticker for SharePoint

We looked at a number of controls to add a stock ticker to our intranet after it was requested that we do so. We looked at a few paid versions, considered building our own and in the end settled for using Google's phenomonal API for stock data.  Sure it is 15 minutes delayed so day trades should go find a version that you pay for.

Sample yerself some XML results here: http://www.google.com/ig/api?stock=.INX  You'll see in line 275 of the attached web part where the call to Google's API lives and can me modified.

SharePoint comes with a great data view web part that we just fed the Google output into, did a little visual tweaking to meet the space constraits we had and we were done.  Elegant, reliable, cheap.   Win.

I attached the exported web part that we used that you can import as a starting point if you'd like or if you want a little more control you can just build your own and not spend much more time in the process.

stock_ticker.webpart

Sunday
Feb032008

Code – Disabling the Enter Key on Web-Forms

An old entry from a few years back, it had a number of hits so it may be useful, qualified for migration...

I got burned a while back by not having this in place when the submit button in a web-app only appeared under certain circumstances. Place this in the top of a web page to squelch the ENTER key in an INPUT=TEXT in your form. Then they gotta use the button when you say that the time is right =)

1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
 
function stopRKey(keyp) {
  var keyp = (keyp) ? keyp : ((event) ? event : null);
  var node = (keyp.target) ? keyp.target : ((keyp.srcElement) ? keyp.srcElement : null);
  if ((keyp.keyCode == 13) && (node.type=="text"))  {return false;}
}
 
document.onkeypress = stopRKey;
 
</script>
Page 1 2 3