InstallShield Tips and Techniques

October 17, 2015

Determining Windows Operating System with VersionNT

Filed under: Reference Materials — shieldmaster @ 1:05 pm

With new Windows 10 Operating System, when you use a version of InstallShield older than version 2015 – you won’t be able to use the pre-defined constants. Here is the table that allows you to use VersionNT for InstallScript evaluation or conditional statements.

 

Windows Operating System

Windows version

Version9x

VersionNT

   

Windows 95

4.0

400

0

   

Windows 98

4.10

410

0

   

Windows Me

4.90

490

0

   

Windows NT4 (32-bit edition)

4.0

0

400

   

Windows 2000 (32-bit edition)

5.0

0

500

   

Windows XP (32-bit edition)

5.1

0

501

   

Windows XP (x64 edition)

5.2

0

502

   

Windows Server 2003 (32-bit edition)

5.2

0

502

   

Windows Server 2003 (x64 or IA64 edition)

5.2

0

502

   

Windows Server 2003 R2 (32-bit edition)

5.2

0

502

   

Windows Server 2003 R2 (x64 or IA64 edition)

5.2

0

502

   

Windows Vista (32-bit edition)

6.0

0

600

   

Windows Vista (x64 edition)

6.0

0

600

   

Windows Server 2008 (x64 or IA64 edition)

6.0

0

600

   

Windows 7 (32-bit edition)

6.1

0

601

   

Windows 7 (x64 edition)

6.1

0

601

   

Windows Server 2008 R2 (x64 or IA64 edition)

6.1

0

601

   

Windows 8 (32-bit edition)

6.2

0

602

   

Windows 8 (x64 edition)

6.2

0

602

   

Windows Server 2012 (x64 edition)

6.2

0

602

   

Windows 8.1 (32-bit edition)

6.3

0

603

   

Windows 8.1 (x64 edition)

6.3

0

603

   

Windows Server 2012 R2 (x64 edition)

6.3

0

603

   

Windows 10 (32-bit edition)

10.0*

0

1000

   

Windows 10 (x64 edition)

10.0*

0

1000

   

Windows Server 2016 (x64 edition)

10.0*

0

1000

   

March 15, 2015

Preserving user configured information from previously installation application

Filed under: Reference Materials — shieldmaster @ 8:56 pm

When your application installed in the field has user configured information, it is imperative that this user data be preserved during Major Upgrades as the newer application is installed.

During Major Upgrade processing, one of the options is that the original application is completely uninstalled (silently), which will destroy this user configured data.

You could have user configured information appearing in any of the following traditional components:

  • Registry entries
  • INI file entries
  • XML document entries
  • File information

Using as an example, Registry Entries, here is the process you will need to follow when preserving registry entries that are found in earlier versions of your application.

  1. Identify the registry entries & whether they are numeric or string entries
  2. Now create System Search Entries to retrieve each Registry Entry into a MSI Property.
  3. Now go to the Property Manager and ensure that the correct default value is entered for the new Entry
  4. Now if the registry value from the original application is a numeric value, we have to add an additional step to resolve the issue that is caused when you send a signed numeric value into a string
  5. Now we need to tie the new MSI Property “ENABLE_ANNOTATION_REG” to the registry entry in the component that installs it.

Now, I will expand on each of these steps..

Process to preserve registry entries

  1. Identify the registry entries & whether they are numeric or string entries. Here is shown an example of registry entries in the InstallShield project:

Reg1

.  Now create System Search Entries to retrieve each Registry Entry into a MSI Property. Select one registry entry, HKEY\Software\Witness Systems\AIM\EnableAnnotation” – a string value and create a System Search entry:

Reg3 Reg2

  1. Now go to the Property Manager and ensure that the correct default value is entered for the new Entry “ENABLE_ANNOTATION_REG”. This is because this value will become the string entry for the registry.  In the case of an existing client – the registry value would be retrieved and stored here.  Then when the new installation is completed, the value found within the registry comes from the previous installation.  If no previous installation exists, the value plugged into the registry is the value from the Property Manager.

Reg4

  1. Now if the registry value from the original application is a numeric value, we have to add an additional step to resolve the issue that is caused when you send a signed numeric value into a string. A numeric value such as “1” is stored as “#1”, which becomes “S1” in a string property.

We need to create a Custom Action “Trim_Values” that executes a simple VBScript that will strip off the “#” sign, such as:

dim value

‘Remove the extra hash off of the dword values

value = Session.Property(“ENABLE_ANNOTATION_REG”)

Session.Property(“ENABLE_ANNOTATION_REG”) = Replace(value,”#”,””)

We need to create a Custom Action with these properties:

Reg5

It’s ok to always execute this – no impact if the value is default because no previous installation was found..

 

  1. Now we need to tie the new MSI Property “ENABLE_ANNOTATION_REG” to the registry entry in the component that installs it. Here is a snapshot of the result:

Reg6

Note how you can see ([ENABLE_ANNOTATION_REG]) is shown in the display, but if you modify the entry you don’t see the reference.  You just have to enter [ENABLE_ANNOTATION_REG] overwriting the default entry.

 

May 7, 2014

Resolution to ActiveX component can’t create object: ‘SAAuto1050.ISWiProject’

Filed under: Reference Materials — shieldmaster @ 4:22 am

Our Build automation was recently switched to a WinServer 2008 Operating System, and we encountered an error when running the InstallShield Automation Object that we used to modify project’s version number. The error “ActiveX component can’t create object” was issued against the Automation “SAAuto1050.dll”. Had to put it aside to handle other more pressing issues, until I was able to deal recently.

Turns out the InstallShield automation is a 32bit process, but the 64bit Operating System was running a 64bit version of CScript – which caused the issue. The easiest solution was to force the execution of the 32bit CScript process. Here is a snippet of an ANT script that builds a VBScript script that is used to update the InstallShield project and inject the current build number:

      
 

    <!– ============================================================================================ –>

    <!—First establish the correct Product Version using the current build number variable. –>

    <!– ============================================================================================ –>

    <property file=”${basedir}\build.number”/>

    <property name=”ProdVersion” value=”${release.number}.${version.number}.${servicepack.number}.${build.number}”/>

 

<!– ============================================================================================ –>

<!– Create the SetAgentScreenCaptureProductVersion.vbs file to take the build number and –>

<!– place it in the ProductVersion property in the ism before the install is built. –>

<!– ============================================================================================ –>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”‘——————————————————————————————-${line.separator}” append=”false”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”‘This Script is designed to take the build number from build.number and place it in${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”‘the .ism file before the install is compiled.${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”‘——————————————————————————————-${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”dim pProject, pProductCode${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”set pProject = CreateObject(&quot;SAAuto1050.ISWiProject&quot;)${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”${line.separator}${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”pProject.OpenProject(&quot;${Agent}\src\install\ScreenCapture.ism&quot;)${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”pProject.ProductVersion = &quot;${ProdVersion}&quot;${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”pProject.SaveProject${line.separator}” append=”true”></echo>

<echo file=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs” message=”pProject.CloseProject${line.separator}” append=”true”></echo>

 

<!– ============================================================================================ –>

<!– Finished creating SetAgentScreenCaptureProductVersion.vbs, now execute with 32bit process. –>

<!– ============================================================================================ –>

    <exec executable=”c:\Windows\SysWOW64\cscript”>

    <arg value=”${Agent}\src\install\SetAgentScreenCaptureProductVersion.vbs”/>

    </exec>

That bit of ANT Script will create a file “SetAgentScreenCaptureProductVersion.vbs” that looks like this:

‘——————————————————————————————-

‘This Script is designed to take the build number from build.number and place it in

‘the .ism file before the install is compiled.

‘——————————————————————————————-

 

dim pProject, pProductCode

set pProject = CreateObject(“SAAuto1050.ISWiProject”)

 

 

pProject.OpenProject(“ScreenCapture.ism”)

pProject.ProductVersion = “1.00.0000.073”

pProject.SaveProject

pProject.CloseProject

 

With a bit of additional work, I could reduce the overhead by quite a bit, but its working correctly now, and there are other items to address.

Hope this helps!

ShieldMaster

February 18, 2014

Kill-Process Custom Actions

Filed under: Reference Materials — shieldmaster @ 11:55 pm

When you have services or processes that are always running in the background, when you decide to uninstall the application you may be presented with this dialog box:


 

This dialog is created by the InstallShield engine after the “CostFinalize” Standard Action is completed.  It recognizes that a process is running that is slated for removal (could be a service or a actively running program) –so it attempts to get the issue resolved before it encounters a “File Locked” scenario.

I would rather kill the running process than allow this dialog box to hang up the installation – plus it seems likely that there will be a customer call to the support center, and that could be bothersome.

 

Option #1: Using the new Kill-Process Custom Action

 

If you are using InstallShield 2012 Spring (or later), they introduced a new Custom Action – the “Kill Process” – as shown here:

 

Here is the help topic on the Kill Process. To summarize:

  • Create a new Custom Action “ISI_KillProcess” – or in my sample “ISI_KillMAP”
  • Sequence it correctly – I wanted it quite early in the sequencing, plus I wanted to run it when the customer ran it silently (without UI Sequence)
  • Create a new MSI Property “ISTerminateProcesses” that contains a list of the processes to kill (if multiple ones, such a previous release names), separate by semicolon (;)

 

 

 

Option #1: Building your own Kill-Process Custom Action

 

If you are using any previous versions of InstallShield 2011 (or earlier), you have to create your own Custom Action to “Kill a Process”. I had typically used an InstallScript Custom Action (scripts downloaded from InstallSite.org ) to check for running processes – but the script ran very slow – and if you are checking for quite a few older versions (such as when you perform a “Major Upgrade” it could take over 5 minutes.

I built this Kill Process Custom Action using the standard “New EXE – Path referencing a directory” – as shown here:

To summarize:

  • Create a new Custom Action “ISI_KillProcess”
  • Sequence it correctly – I wanted it quite early in the sequencing – but the InstallScript engine needs to resolve the working directory “WindowsFolder” – so you have to sequence it after “ResolveSource” in the UI Sequence – and “CostFinalize” in the Execute sequence
  • Select “WindowsFolder”
  • Enter the following “taskkill /F /IM XXXXXX” – where ‘/F’ means to force the kill, ‘/IM’ is the option to provide the process name – and use the full process name (including the .EXE).

 

 

Hope this helps!

Shieldmaster

September 5, 2013

How to determine Windows Server 2012 OS in InstallScript function

Filed under: Reference Materials — shieldmaster @ 10:51 pm

I needed to determine whether I was installing on a Windows Server 2012 – but within the online help for InstallShield 2013, it did not provide the VersionNT/WinBuild/OS Major/OS Minor values that would assist in scripting the function.

I went ahead and created a testbed.MSI that would output the values for each, and then created a sample script to allow you to quickly determine the values.

 

// ************************************************************************************************

//        OS SYSTEM              VersionNT       WindowsBuild          OS Major OS Minor

//        WinServer 2008 R2        601    greater than 7100           6    0

//        Windows 7             601     greater than 7100           6    0

//        Windows 7 SP1        601     greater than 7601        6    0

//        Windows 8             602    greater than 9200        6    0

//        WinServer 2012        602    greater than 9200           6    0

// ************************************************************************************************

 

nzSize = 256;

MsiGetProperty (ISMSI_HANDLE, "VersionNT", szVersionNT, nzSize);

      MessageBox ( "INFO - VersionNT is: " + szVersionNT, INFORMATION);

 

     

// Check if Operating System is Windows 7 or Win2008 R2

if ( szVersionNT = "601") then 

 

      if (SYSINFO.nOSProductType = VER_NT_WORKSTATION) then

          //; for Windows 7, this is TRUE; for Windows Server 2008, it is FALSE.

        szLogMessage = "ISI-->   INFO - Operating System is Windows 7";

        MessageBox (szLogMessage, INFORMATION);    

      else

        szLogMessage = "ISI-->   INFO - Operating System is Windows Server 2008 R2";

        MessageBox (szLogMessage, INFORMATION);    

      endif;

 

endif;

 

// Check if Operating System is Windows 8 or Win2012

if ( szVersionNT = "602") then 

 

      if (SYSINFO.nOSProductType = VER_NT_WORKSTATION) then

          //; for Windows 8, this is TRUE; for Windows Server 2012, it is FALSE.

        szLogMessage = "ISI-->   INFO - Operating System is Windows 8";

        MessageBox (szLogMessage, INFORMATION);    

      else

        szLogMessage = "ISI-->   INFO - Operating System is Windows Server 2012";

        MessageBox (szLogMessage, INFORMATION);    

      endif;

 

endif;

 

Hope this helps!

ShieldMaster

August 11, 2013

Issue with determining Service Pack on Windows 7 64bit OS

Filed under: Reference Materials — shieldmaster @ 2:26 am

Recently I needed to display the Service Pack that was installed on the Operating System. Traditionally I would use the embedded InstallScript command:

nSPValue = (SYSINFO.WINNT.nServicePack);

But for an unknown reason, this was not working for Windows 7 (64bit machines). I decided to go back to basics and retrieve the Service pack value directly from the registry. Since InstallShield Custom Actions are embodied within a 32bit DLL during execution, when you are reading the 64bit section of the registry, you will need to shut off Registry Redirection – otherwise you would be reading the Wow6432Node section. Here is the working InstallScript that reads in the Service Pack from the registry. The value contained in the registry is “Service Pack X” – but I just needed the SP value for my customer dialog, so I added additional script to parse the value:

if (SYSINFO.bIsWow64) then

    //Turn off Registry Redirection if 64bit

    //  Specifies that all future general registry operations affect the 64-bit parts of the registry 

    //  instead of the 32-bit parts of the registry (on a 64-bit system). 

    

    REGDB_OPTIONS = REGDB_OPTION_WOW64_64KEY;      

endif;

 

        // Retrieve the Service Pack

            RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE); 

            szRegKey = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";

            szRegSubKey = "CSDVersion";    // "Service Pack 1"

            nvType = REGDB_STRING;

            nResult = RegDBGetKeyValueEx(szRegKey, szRegSubKey, nvType, szSPLiteral, nvSize) ;

            StrSub (szSPValue, szSPLiteral,13, 1);

            StrToNum (nSPValue, szSPValue);

 

if (SYSINFO.bIsWow64) then

    //Turn off Registry Redirection if 64bit

    //  Specifies that all future general registry operations affect the 64-bit parts of the registry 

    //  instead of the 32-bit parts of the registry (on a 64-bit system). 

    

    REGDB_OPTIONS = REGDB_OPTIONS & ~REGDB_OPTION_WOW64_64KEY ;      

endif;

 

Sometimes you just got to go back to basics!

 

Shieldmaster

June 4, 2013

Setting up Rollback Custom Actions

Filed under: Reference Materials — shieldmaster @ 11:43 pm

Rollback Actions are triggered when the setup encounters an abort situation and the internal “Rollback” script is invoked. Without going into details of the Rollback script – essentially what the setup does to modify the workstation, the rollback script will undo. For example, any files installed will be removed; any shortcuts or registry entries added will also be removed.

But what if you have Custom Actions that serve to modify the workstation – these actions are not written into the internal InstallShield engine’s rollback script. When the setup encounters an abort situation and the rollback script is invoked, these modifications are not removed. In a worst case scenario, they might prevent the setup from being executed again.

The bottom line is – if you have Custom Actions that serve to modify the workstation during the setup, you should create the special “Rollback” Custom Actions that clean up those modifications.

These are some important issues that must be addressed when you create a Rollback Custom Action:

  • Set the Rollback Custom Action to be sequenced before any Deferred Custom Actions that modify the workstation
  • must be Deferred and sequenced after “InstallIntialize” and before “InstallFinalize”
  • Use In-Script Execution “Rollback Execution”
  • For testing, if you are using an InstallScript CA, you can issue a MessageBox to indicate it’s running when invoked

     

To test the Rollback Custom Action, you can create an InstallScript Custom Action, and set the following:

  • Calls simple InstallScript function “ISI_TestRollback”
  • Set as Deferred Execution
  • Sequence between “InstallIntialize” and before “InstallFinalize” and after the Rollback Custom Action
  • Within script just place “return -1” to trigger error routine

 

When the function ISI_TestRollback fails, the InstallScript rollback script is triggered and the Rollback Custom Action should be activated. Then the actions can be handled to clean up the modifications that would normally be left.

 

Hope this helps!

ShieldMaster

 

June 1, 2013

Installing Apache Tomcat v7.x silently with command line options

Filed under: Reference Materials — shieldmaster @ 10:50 pm

I recently have been working on RSA Security installation processes, and one of the tools that needed to be installed was Apache Tomcat v7.x. I was building a simple batch command process that was installing all of the 3rd Party software applications, such as:

  • Microsoft Visual Studio C++ Redistributable (64bit) – (vcredist.msi)
  • JAVA JRE 1.7_15 (jre-7u15-windows-i586.exe)
  • Apache Tomcat v7.0.35 (apache-tomcat-7.0.35.exe)

The first two software applications were essentially MSI packaged applications – so a traditional batch method to execute a MSI was appropriate, such as:

 

REM  ******************************************************************

REM  **  Install Microsoft Visual Studio C++ Redistributable (64bit)   

REM  ******************************************************************

msiexec /i "%BATCH_SCRIPTS_DIR%\3rdPartyApps\VCRedist\vcredist.msi" /qb /Lvoicewarmupx "%BATCH_INSTALL_PATH%\Logs\VC2005SP1Redist.log"

 

REM  ******************************************************************

REM  **  Silently install JAVA JRE 1.7_15                                        

REM  ******************************************************************

"%BATCH_SCRIPTS_DIR%3rdPartyApps\jre-7u15-windows-i586.exe" /s /v"INSTALLDIR="%BATCH_INSTALL_PATH%\JRE" ADDLOCAL=ALL IEXPLORER=1 MOZILLA=1 JAVAUPDATE=0 REBOOT=suppress /L*vx %BATCH_INSTALL_PATH%\Logs\JREInstall.log" " /qn

 

The last software application was something other than a traditional MSI packaged applications – I could not find a method to execute it silently. After quite a bit of research, I think it is packaged as a “NSIS installers”, which are generated by using the ‘MakeNSIS’ program to compile a NSIS script (.NSI) into an installer executable. They use a different set of silent parameters than the traditional MSI packages. This is a link to documentation regarding these parameters.

In my situation, to install Apache Tomcat, I was able to use these parameters:

REM  ******************************************************************

REM  **  Silently install Apache Tomcat v7  

REM  ******************************************************************

"%BATCH_SCRIPTS_DIR%3rdPartyApps\apache-tomcat-7.0.35.exe" /S /D=%BATCH_INSTALL_PATH%\Tomcat

 

The Apache Tomcat installation will automatically locate the JRE installation, therefore does not need to be passed thru the command line.

Hope this helps!

ShieldMaster

July 5, 2012

Checking return code from VBScript Custom Action

Filed under: Reference Materials — shieldmaster @ 9:20 pm

Recently I had the need to run a Custom Action in an older InstallShield Basic MSI project (version 10.5). The Custom Action would verify that a required application product was already installed – and at the correct version level. If either check failed, the Custom Action should return a negative return code and cause the installation to be cancelled.

I did not want to create the Custom Action using InstallScript since that version had the InstallScript Script Engine delivered as a separate MSI (ISScriptEngine.msi), and I wanted no additional MSI requirements. I decided on using VBScript for the Custom Action – but indicated earlier, I needed the VBScript function to return a value that can be used to abort the installation. I have used VBScript often in the past to retrieve CustomActionData and perform activities, but never had to need to issue a return code. Always I would create a VBScript Custom Action with the VBScript embedded in the Custom Action – but I could never could get this script technique to work.

Finally I created a VBScript Custom Action – but stored the VBScript in a .VBS script file.

Here is a snapshot of the Custom Action:

Note that I entered “CheckAgentVersion” in the Script Function line. Here is an extract of the VBScript:

Function  CheckAgentVersion

    Dim    sAgentVersion

       Const  cAgentBaseVersion = "11.01.0001.0000"

    
 

      'MsgBox "Running AgentLP CheckAgentVersion Custom Action"

       LogInfo "======== Running AgentLP CheckAgentVersion Custom Action =========="

       Set objShell = CreateObject("WScript.Shell")

 

       sAgentVersion = Session.Property("AGENT_VERSION")

       If sAgentVersion = "NOT FOUND" Then

        LogInfo "Agent was not installed!" 

        MsgBox "WARNING:  Agent was not installed!  Screen Capture Language Pack will abort!" 

        CheckAgentVersion = -1 

    Else 

           LogInfo "Agent was installed - and version is: " & sAgentVersion   

           If Eval("sAgentVersion > cAgentBaseVersion") then

            LogInfo "Agent installed is proper version!"   

              CheckAgentVersion = 0 

            LogInfo "======== Exiting AgentLP CheckAgentVersion with return code of " & nReturn

            Exit Function 

        Else

              LogInfo "Agent installed is not the proper version!" 

            MsgBox "WARNING:  Agent installed is not the most current version, so Language Pack will abort!      

            CheckAgentVersion = -1                   

            LogInfo "======== Exiting AgentLP CheckAgentVersion with return code of " & nReturn   

            Exit Function

        End If     

    End If     

End Function

 

Function LogInfo(msg)

   dim rec

   Set rec = Session.Installer.CreateRecord(1)

   rec.StringData(0) = msg

   LogInfo = Session.Message(&H04000000, rec)

   'LogInfo is a VBScript function copyrighted (@2011) by Ian Linsdell

End Function

 

By setting the return code to the function name, it will be correctly interrogated by the Custom Action. Granted, you should never pop up a message dialog unless you query the “UserUI” property to see if they are running full dialogs (i.e., not silently). But this should give you a heads up on how to accomplish this.

Also a couple of notes about the VBScript function:

  1. A co-worker Ian Linsdell wrote the slick sub-function that allows you to write directly into the MSI log file. If you use this function, please continue to keep his copyrighted comment!
  2. The MSI Property “AGENT_VERSION” is a result of a System Search entry that looks for the registry entry that confirms the Agent was installed. Obviously if the installation was not found, the default value of “NOT FOUND” would be in the MSI Property.

Regards
ShieldMaster

 

May 27, 2012

Memorial Day 2012

Filed under: Reference Materials — shieldmaster @ 3:05 am

NO, FREEDOM ISN’T FREE
I watched the flag pass by one day.
It fluttered in the breeze.
A young Marine saluted it,
And then he stood at ease.
I looked at him in uniform
So young, so tall, so proud,
With hair cut square and eyes alert
He’d stand out in any crowd.
I thought how many men like him
Had fallen through the years.
How many died on foreign soil?
How many mothers’ tears?
How many pilots’ planes shot down?
How many died at sea?
How many foxholes were soldiers’ graves?
No, freedom isn’t free.
I heard the sound of taps one night,
When everything was still
I listened to the bugler play
And felt a sudden chill.
I wondered just how many times
That taps had meant “Amen,”
When a flag had draped a coffin
Of a brother or a friend.
I thought of all the children,
Of the mothers and the wives,
Of fathers, sons and husbands
With interrupted lives.
I thought about a graveyard
At the bottom of the sea
Of unmarked graves in Arlington.
No, freedom isn’t free.
©Copyright 1981 by CDR Kelly Strong, USCG (Ret).
This poem is important to Kelly because he wrote it as a high school senior (JROTC cadet) at Homestead High, Homestead, FL. in 1981. It is a tribute to his father, a career marine who served two tours in Vietnam. When he finds others trying to take credit for the authorship of the poem, Kelly sees it as a dishonor to the man who inspired the poem, his Dad.
« Newer PostsOlder Posts »

Blog at WordPress.com.