Search

Tuesday, November 27, 2007

NAnt Create SFX Archive Task

I have been working on a installer for a .NET web application with a database back-end. The installer itself deserves a series of posts since it was fairly complicated to get everything to work. An integral part of the installer was using NAnt for various things. The final stage in creating the ready to deploy executable is packaging the files in a self-extracting archive that extracts and runs the setup. With the help of Winrar and NAnt, that task can be automated. Let's get started. Requirements:

WinRar SFX options file sample:

; The path to the setup executable Setup=Setup.msi ; Extract the files to a temporary directory TempMode ; Use semi-silent mode Silent=2 ; Overwrite any existing files Overwrite=1 ; The title of the SFX archive Title=Sample Setup ; The text to show initially when the user clicks on the SFX archive (will only matter if using Silent=0) Text { The installer will extract and run the setup }

NAnt build file sample:

<project name="SampleSFX" default="createSFX" basedir=".">
 <description>createSFX task for creating a sample self-extracting installer</description>

 <!-- The name of the archive to be created -->
 <property name="archiveName" value="SampleSFX" overwrite="false" />

 <!-- The target directory from where the build will be invoked -->
 <property name="targetDirectory" value="." overwrite="false" />

 <!-- The relative path (from the targetDirectory) to directory containing the files to be archived -->
 <property name="archiveDirectory" value="<RelativePathToTheDirectoryToArchive>" overwrite="false">

 <!-- The mask of the file extensions to be archived -->
 <property name="archiveFileMask" value="*.*" overwrite="false" />

 <!-- The path to the winrar executable -->
 <property name="winrarPath" value="c:\program files\winrar\winrar.exe" overwrite="false" />

 <!-- The winrar command line switches for creating the archive -->
 <property name="winrarSwitches" value="a -ep -ep1 -r -sfxdefault.sfx" overwrite="false" />

 <!-- The name of the sfx options file to use while creating the archive -->
 <property name="sfxOptionsFile" value="sfxoptions.txt" overwrite="false" />

 <!-- The relative path (from the targetDirectory) to icon file to be used for the created archive -->
 <property name="sfxIconFile" value="images\setupIcon.ico" overwrite="false" />

 <property name="archiveDirectoryFullPath" value="${path::combine(path::get-full-path(targetDirectory), archiveDirectory)}" overwrite="true" />
 <property name="sfxOptionsFileFullPath" value="${path::combine(path::get-full-path(targetDirectory), sfxOptionsFile)}" overwrite="true" />
 <property name="sfxIconFileFullPath" value="${path::combine(path::get-full-path(targetDirectory), sfxIconFile)}" overwrite="true" />

 <target name="createSFX" description="Creates the self-extracing installer archive">
  <exec
  basedir="."
  program="${winrarPath}"
  commandline="${winrarSwitches} -z&quot;${sfxOptionsFileFullPath}&quot; -iicon&quot;${sfxIconFileFullPath}&quot; &quot;${archiveName}&quot; &quot;${archiveDirectoryFullPath}\${archiveFileMask}&quot;"
  workingdir="."
  failonerror="true" />
 </target>
</project>
To run this NAnt task:
  1. Open a command prompt in the targetDirectory you specified in the above build file.
  2. Run the build file with: nant -buildfile:<nameOfTheAboveBuildFile.build>

This will create a single executable file. When the user runs the executable, the setup files will be extracted and the "Setup.msi" (or whatever you specified in the sfx options file) will be executed.

Wednesday, November 21, 2007

Getting Enabled / Disabled Active Directory Accounts in ColdFusion and T-SQL

While working on recent telephone directory project based on Active Directory, it occurred to me that I am listing all users/accounts in Active Directory regardless of the fact that they might be disabled. Usually, my workplace disables a user's account once he/she is no longer working for the company. So here is how to get a list of only enabled or only disabled accounts from Active Directory. The examples cover doing this through ColdFusion's built in <cfldap> and using T-SQL syntax by leveraging Active Directory as a linked server. I have covered setting up Active Directory as a SQL Server linked server at Querying Active Directory Through SQL Server Here is how this is done with a simple cfldap query. Before you run this code below, please replace the values in the <> This query gets the "distinguishedName", "givenName" and "sn" attributes from Active Directory. The complicated part is the filter it uses. Here is the breakdown:
  • (objectCategory=Person) tells cfldap to get only objects that match the category of "person"
  • (objectClass=User) tells cfldap to get only objects that match the class of "user"
  • (!(userAccountControl:1.2.840.113556.1.4.803:=2)) tells cfldap to get only objects for witch when a bitwise AND is applied to the "userAccountControl" attribute and the number 2, the result does not equal to 0. This is a bit complicated but what it means is that if the bitwise operations does not return 0, the account is not disabled. To specify that only disabled accounts should be returned instead, we would use "(userAccountControl:1.2.840.113556.1.4.803:=2)" instead of "(!(userAccountControl:1.2.840.113556.1.4.803:=2))". I did not come up with this :-) Here is Microsoft's explanation on how to use bitwise filter.
<cfldap
 action="query"
 name="enabledADUsers"
 start="<replaceWithBaseDN>"
 server="<replaceWithActiveDirectoryServer>"
 username="<replaceWithDomainAccount>"
 password="<replaceWithDomainAccountPassword>"
 scope="subtree"
 filter="(&(objectCategory=Person)(objectClass=User)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
 attributes="distinguishedName,givenName,sn" timeout="0">

<cfdump var="#enabledADUsers#" />
Here is the same thing but done through a SQL Server linked server. The filter is the same but written with the T-SQL syntax. The differencs is in how the filter on the "userAccountControl" attribute is applied through T-SQL. In the code below, the line "userAccountControl & 2 = 0" is equvalent to the cfldap filter "(!(userAccountControl:1.2.840.113556.1.4.803:=2))" which specifies that only accounts that are not disabled should be returned in the query. To specify that only disabled accounts should be returned instead, we would use "userAccountControl & 2 <> 0".
select userAccountControl,
 distinguishedName,
 givenName as firstName, sn as lastName,
from openquery(<replaceWithNameOfLinkedServer>, '
 select userAccountControl, distinguishedName, givenName, sn
 from ''LDAP://<replaceWithBaseDN>''
 where objectCategory = ''Person''
 and
 objectClass = ''user''
 order by sn
 ')
where userAccountControl & 2 = 0
Here are some links on the subject:
How to query Active Directory by using a bitwise filter
How to use the UserAccountControl flags to manipulate user account properties
How Can I Get a List of All the Disabled User Accounts in Active Directory?
Retrieving Basic Active Directory Account Settings in C#

Wednesday, November 14, 2007

Useful Prototype.js String Functions

Often while doing web development with JavaScript there is a need to do some kind of string manipulation. Some of the more common tasks are checking if the string is empty and replacing some parts of the string but it doesn't have to stop there. Sometimes I want to capitalize the first letter in a string or check if the string ends with a certain character. In the past I would have to write functions most often involving regular expressions to do each of those trivial tasks. Luckily, in the present my favorite JavaScript library Prototype.js already comes with the mentioned above plus a bunch more string functions. Here are some that I use often:

  • empty() - checks if a string is empty
    var myEmptyString = "";
    if (myEmptyString.empty()) alert ('myEmptyString is empty');
  • blank() - checks if a string is either empty or contains white space
    var myBlankString = " ";
    if (myBlankString.blank()) alert ('myBlankString is blank');
  • capitalize() - capitalizes the first letter in a string
    var myString = "lower_case_string";
    // Will show an alert with "Lower_case_string"
    alert(myString.capitalize());
  • gsub(pattern, replacement) - replaces a pattern in the string with the string specified in 'replacement'
    var myString = "some_string";
    // Will show an alert with "some string"
    alert(myString.gsub('_', ' '));
  • endsWith(substring) - checks if the specified string ends with the provided 'substring'
    var myString = "some string_";
    // Will show an alert with "myString ends with _"
    if (myString.endsWith('_')) alert('myString ends with _');
Many more can be found at the Prototype API reference

Thursday, November 08, 2007

Getting Started with NAnt - .NET Build Tool

As part of working on a installer for .NET web application, I decided to use NAnt (stands for Not Ant) which is a .NET equivalent of the popular Java based ANT build tool. If you don't know what a build tool is, check out the Build Tool article on Wikipedia. The beauty of NAnt is in the fact that you can automate many tasks and once you have a working build file, you do not have to worry about it again. Here is some of you can do with the help of NAnt:
  • Install and register ASP.NET with IIS without user intervention
  • Start/stop/pause or install windows services
  • Zip or unzip files
  • Changes values inside text or xml files
  • Create files and/or directories
  • Copy/move files and/or directories
  • Set directory and/or file security permissions
  • Much more
  • If that is not enough, you can always extend NAnt with your own custom tasks (written in VB.NET or C#)
That being said, here are the steps to get started with NAnt:
  1. Get NAnt from http://prdownloads.sourceforge.net/nant/nant-0.85-bin.zip?download
  2. Get NAntContrib from http://prdownloads.sourceforge.net/nantcontrib/nantcontrib-0.85-bin.zip?download
  3. Extract NAnt in "C:\Program Files\NAnt"
  4. Extract NAntContrib\bin in "C:\Program Files\NAnt\bin"
  5. Create a NAnt.bat file in C:\Windows with the following:
    @echo off
    "C:\Program Files\NAnt\bin\nant.exe" %*
  6. Create and run a sample build file. Build files can perform many functions, from compiling your .NET application to downloading files from the net. For a list of Tasks, see the NAnt Tasks list and the NAntContrib Tasks list.
Below is a sample build file that takes a currently installed web application, pre-compilies it and creates a zip file of the pre-compiled application ready to be deployed:
<?xml version="1.0"?>
<project name="Ensemble" default="deploy" basedir="." xmlns="http://nant.sf.net/schemas/nant.xsd">
 <description>Precompiles and zips the Ensemble project</description>

  <property name="debug" value="true" overwrite="true" />

 <!-- The target directory where the application will be deployed and the zip file created -->
  <property name="targetDirectory" value="." overwrite="false" />

 <!-- The temporary directory where the web application will be precompiled -->
  <property name="deployTarget" value="${path::combine(path::get-full-path(targetDirectory), 'deploy')}" overwrite="true" />

  <!-- The virtual directory that the web application resides in -->
  <property name="virtualDirectory" value="webApplication" overwrite="true" />

 <!-- The name of the zip file to create when zipping the deployed web application -->
  <property name="deployZipFilename" value="webApplication.zip" overwrite="true" />

 <!-- The location where the zip file will be created -->
 <property name="deployZipFileLocation" value="${path::combine(path::get-full-path(targetDirectory), deployZipFilename)}" overwrite="true" />

 <!-- The location of the .NET Framework directory (for version 2) -->
  <property name="dotnetLocation" value="${framework::get-framework-directory('net-2.0')}" overwrite="true" />

  <!-- 'clean' target deletes the previously created zip file and deploy directory -->
  <target name="clean" description="Remove all generated files">
  <!-- Delete the existing zip file -->
    <delete file="${deployZipFileLocation}" if="${file::exists(deployZipFileLocation)}" />
  <!-- Delete the existing deploy directory -->
    <delete dir="${deployTarget}" if="${directory::exists(deployTarget)}" />
  </target>

  <!-- 'build' target precompiles this ASP.Net application into the deployTarget directory -->
  <target name="deploy" description="Precompiles the web application and creates a zip file for it" depends="clean">
    <!-- Precompile the web application with the built-in .NET utility -->
    <exec
     basedir="."
     program="${dotnetLocation}\aspnet_compiler.exe"
     commandline="-nologo -fixednames -v ${virtualDirectory} "${deployTarget}""
     workingdir="."
     failonerror="true" />

    <!-- Clean up the "deployTarget" directory -->
  <delete>
   <fileset>
    <!-- Delete any visual studio related files -->
    <include name="${deployTarget}/*.TempSolution" />
    <include name="${deployTarget}/**/*.scc" />
    <include name="${deployTarget}/**/*.resx" />
    <include name="${deployTarget}/**/*.txt" />
    <include name="${deployTarget}/**/*.db" />
    <include name="${deployTarget}/**/*.vssscc" />
    <!-- Delete all the files in the upload and files directory -->
    <include name="${deployTarget}/upload/**/*" />
    <include name="${deployTarget}/files/**/*" />
   </fileset>
  </delete>

    <!-- Create a zip file from precompiled web application -->
    <zip zipfile="${deployZipFileLocation}" includeemptydirs="true">
      <fileset basedir="${deployTarget}">
        <include name="**/*" />
      </fileset>
    </zip>

    <!-- delete the deployTarget directory -->
    <delete dir="${deployTarget}" />
  </target>
</project>
// //]]>