Category Archives: Scripting

Java SE Embedded package for Synology NAS

UPDATE 4 – Now supports DS414j!
UPDATE 3 – Now with Java 8, and full support for all versions on Intel CPUs running DSM 5.0!
UPDATE 2 – Now running on PowerPC 2010 Synology models running DSM 5.0 beta!
UPDATE – Now running on Armada 370/XP (ARMv7) and Intel Atom Evansport CPUs, as used in some x14 series Synology models!

There is a lot of good Java software, but it’s difficult to get Java running on a NAS. I decided to do something about that, mainly to pave the way for a Serviio package that didn’t require a huge guide to install. To stay legal, I followed how QNAP packaged Java for their NAS products. The Java binaries cannot be redistributed so this package looks for the downloaded .tar.gz archive in the public shared folder on the NAS. The end user is required to register with Oracle to get the file, as you can see from the description below. I also added locale support to the underlying Linux since Java requires this for UTF-8 support when reading from the filesystem.

Only Marvell Kirkwood, Marvell Armada 370/XP, Intel, and Freescale QorIQ/PowerQUICC PowerPC CPUs are supported, so please check which CPU your NAS has. Java 7 for Intel Atom CPU does not currently work on Synology systems running DSM versions older than 5.0, so those users are limited to Java 6. Though Oracle have published a JRE for PowerPC, it also requires DSM 5.0. PowerQUICC PowerPC synos running older DSM versions do not have a recent enough glibc version (2.3.4, but Oracle’s Java needs 2.4). It is technically possible to run Java on those units, but it requires chroot-ing to a Debian install which can get complicated. Christophe from chreggy.fr has recently released packages to automate this.

Java package for Synology

 

Instructions

  • If you have a multi-bay NAS, use the Shared Folder control panel to create a shared folder called public (it must be all lower case). On single bay models this is created by default. Assign it with Read/Write privileges for everyone.
  • Register with Oracle and download Java SE for Embedded 7 for either ARMv5, ARMv7 Headless Server, or PowerPC e500v2 as appropriate (Intel NAS users will need the end-of-life Java SE for Embedded 6), and save in the public shared folder. If the file isn’t found an error message will display the full expected filename. You cannot use a different version by renaming it, since the folder structure inside the archive will be different.
  • Install the Java package from the DSM Package Center. In Settings -> Package Sources add my package repository URL which is http://packages.pcloadletter.co.uk. My packages will appear in the Community section once the repo URL is added.
  • If you have trouble getting the Java archive recognised, try downloading it with a different web browser. Some browsers try to help by uncompressing the file, or renaming it without warning. I have tried to code around most of these behaviours.
  • The package does not need to start, just installing it is sufficient.
  • If you update DSM later, you will need to re-install this package or else UTF-8 and locale support will be broken by the update.
 

Package scripts

For information, here are the package scripts so you can see what it’s going to do. You can learn more about how Synology packages work by reading the Synology Package wiki.

installer.sh

#!/bin/sh

#--------JAVA installer script
#--------package maintained at pcloadletter.co.uk

COMMENT="# Synology Java Package"
SYNO_CPU_ARCH="`uname -m`"
[ "`echo ${SYNO_CPU_ARCH} | cut -c1-7`" == "armv5te" ] && SYNO_CPU_ARCH="armv5tel"
cat /proc/cpuinfo | grep "Comcerto 2000" > /dev/null && SYNO_CPU_ARCH="armv7l-neon"
[ "${SYNO_CPU_ARCH}" == "x86_64" ] && SYNO_CPU_ARCH="i686"

if [ "${SYNOPKG_PKGNAME}" == "java6" ]; then
  DOWNLOAD_URL="http://tinyurl.com/javaembedarchive"
  EXTRACTED_FOLDER="ejre1.6.0_38"
  if [ "`echo ${SYNO_CPU_ARCH} | cut -c1-7`" == "armv5te" ]; then
    JAVA_BINARY="ejre-1_6_0_38-fcs-b05-linux-arm-sflt-eabi-headless-13_nov_2012.tar.gz"
    JAVA_BUILD="ARMv5 Linux - Headless EABI"
  elif [ "${SYNO_CPU_ARCH}" == "armv7l" ]; then
    JAVA_BINARY="ejre-1_6_0_38-fcs-b05-linux-arm-vfp-eabi-headless-13_nov_2012.tar.gz"
    JAVA_BUILD="ARMv6/7 Linux - Headless EABI, VFP, SoftFP ABI, Little Endian"
  elif [ "${SYNO_CPU_ARCH}" == "i686" ]; then
    JAVA_BINARY="ejre-1_6_0_38-fcs-b05-linux-i586-headless-13_nov_2012.tar.gz"
    JAVA_BUILD="x86 Linux Small Footprint - Headless"
  elif [ "${SYNO_CPU_ARCH}" == "ppc" ]; then
    JAVA_BINARY="ejre-1_6_0_38-fcs-b05-linux-ppc-e500v2-headless-13_nov_2012.tar.gz"
    JAVA_BUILD="Power Architecture Linux - Headless - e500v2 core"
  fi    

elif [ "${SYNOPKG_PKGNAME}" == "java7" ]; then
  DOWNLOAD_URL="http://tinyurl.com/javaembed"
  EXTRACTED_FOLDER="ejre1.7.0_60"
  if [ "${SYNO_CPU_ARCH}" == "armv5tel" ]; then
    JAVA_BINARY="ejre-7u60-fcs-b19-linux-arm-sflt-headless-07_may_2014.tar.gz"
    JAVA_BUILD="ARMv5 Linux - Headless EABI, SoftFP ABI, Little Endian"
  elif [ "${SYNO_CPU_ARCH}" == "armv7l" ]; then
    JAVA_BINARY="ejre-7u60-fcs-b19-linux-arm-vfp-sflt-client_headless-07_may_2014.tar.gz"
    JAVA_BUILD="ARMv6/7 Linux - Headless - Client Compiler EABI, VFP, SoftFP ABI, Little Endian"
  elif [ "${SYNO_CPU_ARCH}" == "armv7l-neon" ]; then
    JAVA_BINARY="ejre-7u60-fcs-b19-linux-arm-vfp-hflt-client_headless-07_may_2014.tar.gz"
    JAVA_BUILD="ARMv6/7 Linux - Headless - Client Compiler EABI, VFP, HardFP ABI, Little Endian"
  elif [ "${SYNO_CPU_ARCH}" == "i686" ]; then
    JAVA_BINARY="ejre-7u60-fcs-b19-linux-i586-headless-07_may_2014.tar.gz"
    JAVA_BUILD="x86 Linux Small Footprint - Headless"  
  elif [ "${SYNO_CPU_ARCH}" == "ppc" ]; then
    JAVA_BINARY="ejre-7u60-fcs-b19-linux-ppc-e500v2-headless-07_may_2014.tar.gz"
    JAVA_BUILD="Power Architecture Linux - Headless - e500v2 with double-precision SPE Floating Point Unit"
  fi

elif [ "${SYNOPKG_PKGNAME}" == "java8" ]; then
  DOWNLOAD_URL="http://tinyurl.com/javaembed"
  EXTRACTED_FOLDER="ejdk1.8.0_06"
  if [ "${SYNO_CPU_ARCH}" == "armv5tel" ]; then
    JAVA_BINARY="ejdk-8u6-fcs-b23-linux-arm-sflt-12_jun_2014.tar.gz"
    JAVA_BUILD="ARMv5 Linux - Headless EABI, SoftFP ABI, Little Endian"
  elif [ "${SYNO_CPU_ARCH}" == "armv7l" ]; then
    JAVA_BINARY="ejdk-8u6-fcs-b23-linux-arm-vfp-sflt-12_jun_2014.tar.gz"
    JAVA_BUILD="ARMv6/7 Linux - VFP, SoftFP ABI, Little Endian"
  elif [ "${SYNO_CPU_ARCH}" == "armv7l-neon" ]; then
    JAVA_BINARY="ejdk-8u6-fcs-b23-linux-arm-vfp-hflt-12_jun_2014.tar.gz"
    JAVA_BUILD="ARMv7 Linux - VFP, HardFP ABI, Little Endian"
  elif [ "${SYNO_CPU_ARCH}" == "i686" ]; then
    JAVA_BINARY="ejdk-8u6-fcs-b23-linux-i586-12_jun_2014.tar.gz"
    JAVA_BUILD="x86 Linux Small Footprint - Headless"
  elif [ "${SYNO_CPU_ARCH}" == "ppc" ]; then
    JAVA_BINARY="ejdk-8u6-fcs-b23-linux-ppc-e500v2-12_jun_2014.tar.gz"
    JAVA_BUILD="Power Architecture Linux - Headless - e500v2 with double-precision SPE Floating Point Unit"
  fi
fi

JAVA_BINARY=`echo ${JAVA_BINARY} | cut -f1 -d'.'`
PUBLIC_FOLDER="`cat /usr/syno/etc/smb.conf | sed -r '/\/public$/!d;s/^.*path=(\/volume[0-9]{1,4}\/public).*$/\1/'`"
TEMP_FOLDER="`find / -maxdepth 2 -name '@tmp' | head -n 1`"
NATIVE_BINS_URL="http://packages.pcloadletter.co.uk/downloads/java-native-${SYNO_CPU_ARCH}.tgz"   
NATIVE_BINS_FILE="`echo ${NATIVE_BINS_URL} | sed -r "s%^.*/(.*)%\1%"`"
#DSM versions older than 4.3 need locale support adding, don't download unless needed
[ ! -e /usr/bin/locale ] && INSTALL_FILES="${NATIVE_BINS_URL}"
source /etc/profile


preinst ()
{
  if [ -z ${PUBLIC_FOLDER} ]; then
    echo "A shared folder called 'public' could not be found - note this name is case-sensitive. "
    echo "Please create this using the Shared Folder DSM Control Panel and try again."
    exit 1
  fi

  JAVA_BINARY_FOUND=

  if [ ! -z ${JAVA_HOME} ]; then
    echo "It seems from /etc/profile that a Java Runtime is already installed at ${JAVA_HOME}. Uninstall it and try again."
    exit 1
  fi
  
  [ -f ${PUBLIC_FOLDER}/${JAVA_BINARY}.tar.gz ] && JAVA_BINARY_FOUND=true
  [ -f ${PUBLIC_FOLDER}/${JAVA_BINARY}.tar ] && JAVA_BINARY_FOUND=true
  [ -f ${PUBLIC_FOLDER}/${JAVA_BINARY}.tar.tar ] && JAVA_BINARY_FOUND=true
  [ -f ${PUBLIC_FOLDER}/${JAVA_BINARY}.gz ] && JAVA_BINARY_FOUND=true
  
  if [ -z ${JAVA_BINARY_FOUND} ]; then
    echo "Java binary bundle not found. " > $SYNOPKG_TEMP_LOGFILE
    echo "I was expecting the file ${PUBLIC_FOLDER}/${JAVA_BINARY}.tar.gz. "
    echo "Please agree to the Oracle licence at ${DOWNLOAD_URL}, then download the '${JAVA_BUILD}' package"
    echo "and place it in the 'public' shared folder on your NAS. This download cannot be automated even if "
    echo "displaying a package EULA could potentially cover the legal aspect, because files hosted on Oracle's "
    echo "server are protected by a session cookie requiring a JavaScript enabled browser."
    exit 1
  fi

  cd ${TEMP_FOLDER}
  for WGET_URL in ${INSTALL_FILES}
  do
    WGET_FILENAME="`echo ${WGET_URL} | sed -r "s%^.*/(.*)%\1%"`"
    [ -f ${TEMP_FOLDER}/${WGET_FILENAME} ] && rm ${TEMP_FOLDER}/${WGET_FILENAME}
    wget ${WGET_URL}
    if [[ $? != 0 ]]; then
      if [ -d ${PUBLIC_FOLDER} ] && [ -f ${PUBLIC_FOLDER}/${WGET_FILENAME} ]; then
        cp ${PUBLIC_FOLDER}/${WGET_FILENAME} ${TEMP_FOLDER}
      else     
        echo "There was a problem downloading ${WGET_FILENAME} from the official download link, "
        echo "which was \"${WGET_URL}\" "
        echo "Alternatively, you may download this file manually and place it in the 'public' shared folder. "
        exit 1
      fi
    fi
  done
  
  exit 0
}


postinst ()
{
  WORKING_DIR=`pwd`
  
  #ldd tool is very useful but not included in DSM
  [ ! -e /bin/ldd ] && cp ${SYNOPKG_PKGDEST}/tools/ldd-${SYNO_CPU_ARCH} /bin/ldd

  #DSM versions older than 4.3 need locale support adding
  if [ ! -e /usr/bin/locale ]; then
    #extract native binaries
    cd ${SYNOPKG_PKGDEST}
    tar xzf ${TEMP_FOLDER}/${NATIVE_BINS_FILE} && rm ${TEMP_FOLDER}/${NATIVE_BINS_FILE}

    #build missing locale with UTF-8 support (don't think it matters which language)
    cp ${SYNOPKG_PKGDEST}/bin/* /bin
    cp -R ${SYNOPKG_PKGDEST}/share/i18n /usr/share
    if [ ! -d /usr/lib/locale ]; then
      mkdir /usr/lib/locale
    fi
    localedef -c -f UTF-8 -i en_US en_US.utf8
  fi  

  #extract Java (Web browsers love to interfere with .tar.gz files)
  cd ${PUBLIC_FOLDER}
  if [ -f ${JAVA_BINARY}.tar.gz ]; then
    #Firefox seems to be the only browser that leaves it alone
    tar xzf ${JAVA_BINARY}.tar.gz
  elif [ -f ${JAVA_BINARY}.gz ]; then
    #Chrome
    tar xzf ${JAVA_BINARY}.gz
  elif [ -f ${JAVA_BINARY}.tar ]; then
    #Safari
    tar xf ${JAVA_BINARY}.tar
  elif [ -f ${JAVA_BINARY}.tar.tar ]; then
    #Internet Explorer
    tar xzf ${JAVA_BINARY}.tar.tar
  fi
  
  #install Java
  if [ "${EXTRACTED_FOLDER}" != "${EXTRACTED_FOLDER/jdk/}" ]; then
    mv ${EXTRACTED_FOLDER} ${SYNOPKG_PKGDEST}
    JRE_PATH="`find ${SYNOPKG_PKGDEST}/${EXTRACTED_FOLDER} -name jre`"
  else
    mv ${EXTRACTED_FOLDER} ${SYNOPKG_PKGDEST}/jre
    JRE_PATH="${SYNOPKG_PKGDEST}/jre"
  fi
  
  #change owner of folder tree
  chown -R root:root ${SYNOPKG_PKGDEST}

  echo "NOTE - This package does *not* start and stop like other packages. "
  echo "Java is correctly installed if you can see the runtime and HotSpot version numbers, "
  echo "and locale information in the package Log tab."
  
  exit 0
}


preuninst ()
{
  exit 0
}


postuninst ()
{
  #clean up profile mods
  sed -i "/${COMMENT}/d" /etc/profile
  sed -i "/${COMMENT}/d" /root/.profile

  #leave locale support in place on older DSM versions - too risky to delete system binaries
  
  exit 0
}
 

start-stop-status.sh

#!/bin/sh

JRE_PATH="`find ${SYNOPKG_PKGDEST} -name jre`"
COMMENT="# Synology Java Package"

EnvCheck()
#updates to DSM will reset these changes so check them each startup 
{
  #/etc/profile should contain 5 lines added by this package tagged with trailing comments
  COUNT=`grep -c "$COMMENT$" /etc/profile`
  if [ $COUNT != 5 ]; then

    #remove any existing mods
    sed -i "/${COMMENT}/d" /etc/profile

    #add required environment variables
    echo "PATH=\$PATH:${JRE_PATH}/bin ${COMMENT}" >> /etc/profile
    echo "JAVA_HOME=${JRE_PATH} ${COMMENT}" >> /etc/profile
    echo "CLASSPATH=.:${JRE_PATH}/lib ${COMMENT}" >> /etc/profile
    echo "LANG=en_US.utf8 ${COMMENT}" >> /etc/profile
    echo "export CLASSPATH JAVA_HOME LANG PATH ${COMMENT}" >> /etc/profile
  fi

  #/root/.profile should contain 3 lines added by this package tagged with trailing comments
  COUNT=`grep -c "$COMMENT$" /root/.profile`
  if [ $COUNT != 3 ]; then

    #remove any existing mods
    sed -i "/${COMMENT}/d" /root/.profile

    #add required environment variables
    echo "PATH=\$PATH:${JRE_PATH}/bin ${COMMENT}" >> /root/.profile

    #set the current timezone for Java so that log timestamps are accurate
    #we need to use the modern timezone names so that Java can figure out DST
    #this should be configured in each Java daemon's start script to that changes
    #to timezone settings in DSM are reflected
    #we need to use the modern timezone names so that Java can figure out DST

    SYNO_TZ=`cat /etc/synoinfo.conf | grep timezone | cut -f2 -d'"'`
    SYNO_TZ=`grep "^${SYNO_TZ}" /usr/share/zoneinfo/Timezone/tzname | sed -e "s/^.*= //"`
    echo TZ=\'${SYNO_TZ}\' ${COMMENT} >> /root/.profile
    echo "export PATH TZ ${COMMENT}" >> /root/.profile
  fi
}

case $1 in
  start)
    EnvCheck
    source /etc/profile
    source /root/.profile

    #evidence of whether Java can start successfully is written to the package log
    java -version > ${SYNOPKG_PKGDEST}/output.log 2>&1
    echo >> ${SYNOPKG_PKGDEST}/output.log
    echo System installed locales: >> ${SYNOPKG_PKGDEST}/output.log
    locale -a >> ${SYNOPKG_PKGDEST}/output.log
    echo >> ${SYNOPKG_PKGDEST}/output.log
    echo JAVA_HOME=$JAVA_HOME >> ${SYNOPKG_PKGDEST}/output.log
    echo TZ=$TZ >> ${SYNOPKG_PKGDEST}/output.log

    exit 0
  ;;

  stop)
    exit 0
  ;;

  status)
    source /etc/profile
    source /root/.profile
    if [ -e "${JAVA_HOME}/bin/java" ]; then
      exit 0
    else
      exit 1
    fi
  ;;

  log)
    echo "${SYNOPKG_PKGDEST}/output.log"
    exit 0
  ;;

esac
 

Changelog:

  • 0027 Updated Java 8 to 8u6m, added support for Mindspeed Comcerto 2000 CPU in DS414j for Java 7 and Java 8
  • 0026 DSM updates will no longer break Java
  • 0025 Updated to Java 7u60
  • 0024 Updated to Java 7u55
  • 0023 Added Java 8 support
  • 0022 Updated to Java 7u51
  • 0021 Locale support is no longer downloaded and added to DSM 4.3 or newer since it’s already present
  • 0020 Added support for Intel Atom Evansport and Armada XP CPUs in new DSx14 products, removed Early Access JDK 8 support since it did not run on the Armada SoCs
  • 0019 Updated to Java 7u45
  • 0018 Updated to Java 7u40, and JDK8 Early Access b106
  • 0017 For Armada370 CPU changed from ARMv7 Server JVM to Client JVM since the Server one turned out to be unstable. Added support for Early Access JDK 8 for Armada 370.
  • 0016 Added Armada370 CPU support (DS213j)
  • 0015 Updated to Java 7u21, Java 6 Embedded seems to be no longer maintained by Oracle
  • 0014 Fixed metadata for DSM 4.2 Package Center
  • 013 Updated to Java 6u38 and Java 7u10
  • 012 Installer script fetches native binaries separately to reduce bandwidth on repo hosting
  • 011 Added support for Freescale QorIQ PowerPC CPUs used in some Synology x13 series products, PowerQUICC PowerPC processors in previous Synology generations with older glibc versions are not supported
  • 010 updated to Java 6u34 and Java 7u6
  • 009 corrected timezone bug for Atlantic and Pacific timezones which may have caused error messages when in fact the package had installed correctly
  • 008 unified the build scripts
  • 007 included the locale binaries to simplify installation, fixed environment variables for root user (inc. timezone with DST support), displayed Java version information and env vars in Log tab, incremented Java to latest versions 6u32 and 7u4, fixed detection of renamed .tar.gz archives downloaded by Google Chrome
  • 006 fixed path issue for root user on non-bootstrapped systems, updated info link to refer back to this page, and improved description text
  • 005 incremented JRE versions to match new releases from Oracle
  • 004 forced check for existence of ‘public’ shared folder
  • 003 fixed a stupid typo that stopped 002 from working :)
  • 002 rewrote scripts to be easier to maintain, and to allow for web browsers that untar or rename the Java and toolchain binary bundles as they’re saved
  • 001 fixed package repo support, defined as a non-runnable service, prevented more than one JRE being installed, saved files into actual package folder rather than @appstore/java now that package names can be separate from Package Center display names
  • v3 initial spk release (I think!)
 
 

Batch script for recursive FFmpeg transcoding

I recently had to shrink around 50GB of MP3 audio recordings that were sitting in a nested folder structure on a web server. Having experimented to find more appropriate LAME encoder settings for spoken word content I needed to transcode the files whilst keeping the existing ID3 tags intact. FFmpeg can do this nicely using libmp3lame, whereas LAME by itself cannot. Armed with my own compile of FFmpeg, I created a drag & drop batch script to recursively work through a folder structure transcoding MP3 and WAV files and writing out the resulting MP3 files in the same folder structure with an amended top level folder name. It will accept multiple files or folders being dragged and dropped. You could adapt this script to whatever task you’re using FFmpeg for.

::MP3 transcoding to more sensible default quality settings for spoken word
::32kHz mono VBR @ approx. 64Kbps
::
::pcloadletter.co.uk

@echo off
setlocal ENABLEDELAYEDEXPANSION
::parse multiple command lines (so multiple targets can be dragged and dropped at once)
:commandlineloop
if "%~1"=="" goto :continue
call :mainloop %1
shift
goto :commandlineloop
:continue
echo.
pause
goto :eof

::main loop we run for each command line arg of the script
:mainloop
set folderpath=
if "%~1"=="" (
  echo Drag and drop files or folders onto this script.
  goto :eof
)
set folderpath=%~1
echo "%1" | find /I ".mp3" && (
  goto :fileonly
)
echo "%1" | find /I ".wav" && (
  goto :fileonly
)

::folders
echo.
echo.
echo Processing folder %1
echo ________________________________________________________________________________
echo.

::we need to find the last folder level in the folderpath
::batch scripting is horrible so we need to use a hacky way to get the last token
::(from http://stackoverflow.com/questions/5473840/last-token-in-batch-variable)
set temp_string=%~1
set count=0

::iterate parsing the tokens and trimming the string until it's empty, while counting how many loops
:loopcounter
for /F "tokens=1* delims=\" %%a in ( "%temp_string%" ) do (
  set /A count+=1
  set temp_string=%%b
  goto loopcounter
)
for /F "tokens=%count% delims=\" %%i in ( "%~1" ) do set folder=%%i

::append the top level folder name with "-optimized" so we create a new folder tree
for /R "%folderpath%" %%i in (*.mp3 *.wav) do (
  set sourcepath=%%~di%%~pi
  set destpath=!sourcepath:%folder%=%folder%-optimized!
  md "!destpath!"
  ffmpeg -i "%%~fi" -acodec libmp3lame -aq 8 -ar 32000 -ac 1 "!destpath!%%~ni.mp3"
)
goto :eof

::individual files
:fileonly
for %%i in ("%folderpath%") do (
  ffmpeg -i "%folderpath%" -acodec libmp3lame -aq 8 -ar 32000 -ac 1 "%%~di%%~pi%%~ni-optimized.mp3"
)
 

There are several neat little tricks here. Multiple folders can be dragged onto the script because it uses SHIFT to work through each command line argument. The next hurdle is to get the last part of the folder name(s) being dragged. Though this would be simple in any Unix-like shell, but to do this in batch without relying on any additional tools proved to be quite tricky. Though tokens can be parsed by FOR, finding the last one (which we need) requires us to count how many tokens there are. Only then can the last one be selected. The folder name string substitution is done by the SET command (also used in my file renaming script). Delayed variable expansion means that the variables between the the exclamation marks are evaluated once per loop rather than the default of once per script execution (more info here). I hadn’t realised until today that you can create a folder structure several layers deep with a single MD command. This avoids having to iterate through all subfolders – we can use FOR’s /R switch to handle the recursion in a single line. For more information on some of the variables containing tildes like %%~pi and %%~ni, try running FOR /?. The script also first runs the FFmpeg subroutine with ECHO in front of the commands so you can double-check the syntax before proceeding.

There are of course much more efficient ways of compressing spoken word content than MP3 these days – AAC-HEv2 for instance, but that will rule out all but the latest audio playback devices.
The source MP3 recordings were in 44.1kHz mono @ 128Kbps. This was definitely overkill for speech. Just by opting for 64Kbps I could halve that. However I couldn’t drop the sample rate below 32kHz, as this is the lowest legal limit of MPEG-1 layer III. This info is tricky to find but the LAME encoder reveals it in its extended help:

MPEG-1   layer III sample frequencies (kHz):  32  48  44.1
bitrates (kbps): 32 40 48 56 64 80 96 112 128 160 192 224 256 320
MPEG-2   layer III sample frequencies (kHz):  16  24  22.05
bitrates (kbps):  8 16 24 32 40 48 56 64 80 96 112 128 144 160
MPEG-2.5 layer III sample frequencies (kHz):   8  12  11.025
bitrates (kbps):  8 16 24 32 40 48 56 64

 

I think it’s safe to assume that most MP3 players can handle variable bit rate. As far as I can remember only the first generation ones couldnt from early last decade – the sort of ones which only had around 64MB of storage. I don’t think excluding players this old is much of a concern.

Playing around with VBR quality settings in LAME we can see that with -V 9 we get a small file (12.9MB becomes 3.7MB) however it’s actually an MPEG-2 layer III file which as you can see above supports lower sample rates. I’m not certain of the wider compatibility of this sub-type of MP3 file, and besides it does sound considerably worse than the lowest MPEG-1 layer III settings which is –V 8 (12.9MB becomes 5.2MB). This proved to be the best compromise between size and quality.

Windows 7 new user profile – MSOE.DLL could not be loaded error

Since my Windows 7 deployment I get an error every time a new user logs in (or logs in following a profile reset).

Windows Mail could not be started because MSOE.DLL could not be loaded

Since you can click past it I wasn’t initially too concerned but I soon came to realize that there’s pretty much no information on the Web about this problem. Anything the search engines turn up is invariably about Outlook Express on Windows 98/Me.

It seems to be caused when some kind of IE First Run process opens Internet Explorer with an MSN page and presumably tries to register the default email client (which is not yet set to Outlook, despite it being installed). The strange thing is that Windows Mail is not even available on Windows 7. Microsoft discontinued it after Vista in favour of Windows Live Mail. WinMail.exe is present on the system but it’s hidden, and that MSOE.DLL has been purposely omitted. Apparently you can get it working again by supplying a copy from a Vista PC.

The problem is that this error looks important enough for a new user to contact the helpdesk about. The other issue is that it significantly delays that first logon, sometimes by several entire minutes. The error occurs immediately after you enter your credentials (you can hear the alert sound) but it happens while the desktop is still hidden from view by the Windows 7 splash screen. Eventually the Preparing your desktop splash screen times out and you can click OK:

Windows Mail could not be started because MSOE.DLL could not be loaded

Notice the label in that floating window – it’s setting up a component which is titled as Microsoft Windows.

Eventually I discovered a KB article relating to Internet Explorer 5 personalized settings, which was when Microsoft introduced this method of installation.

Having consulted that document I viewed HKLM\SOFTWARE\Microsoft\ActiveSetup\InstalledComponents in Regedit and saw that there is indeed a component with the name “Microsoft Windows” which is invoking WinMail.exe, intending to register it as a mail and news (nntp) client:

WinMail.exe ActiveSetup parameters

I have no idea why it’s doing this since WinMail.exe is intentionally disabled in Windows 7. It would seem logical that deleting this whole key should fix the problem (I backed it up first of course). In testing this didn’t seem to work though.

However I did notice that sometimes I got the error on screen twice and other times only once. Exasperated I searched the registry for instances of WinMail.exe until I discovered the reason: the entire ActiveSetup branch of the registry also exists under HKLM\Software\WOW6432Node\Microsoft, for the 32bit version of Internet Explorer on 64bit systems. Once both keys had been deleted the error disappeared.

I was then able to refine this – instead of deleting the whole key you only need to set to the single DWORD value IsInstalled to zero. Do this to both instances and the problem is fixed! In my environment I used my existing VBScript startup script which worktations inherit by Group Policy. Excerpt below:

If InStr (strOS,"Microsoft Windows 7") Then
  'Prevent Windows from trying to invoke the non-existent Windows Mail for new user profile setup
  On Error Resume Next
  strKey = "SOFTWARE\Microsoft\Active Setup\Installed Components\{44BBA840-CC51-11CF-AAFA-00AA00B6015C}"
  objReg.SetDWORDValue HKEY_LOCAL_MACHINE,strKey,"IsInstalled",0
  strKey = "SOFTWARE\Wow6432Node\Microsoft\Active Setup\Installed Components\{44BBA840-CC51-11CF-AAFA-00AA00B6015C}"
  objReg.SetDWORDValue HKEY_LOCAL_MACHINE,strKey,"IsInstalled",0
  On Error Goto 0

Enabling Network Level Authentication on Windows XP by script

Migrating to Windows 7 has thrown up another problem – users wanting to connect from home computers running XP cannot use the Remote Desktop Client to connect to their newly upgraded office PCs. The Network Level Authentication change to the Remote Desktop Client was made because the original RDP is susceptible to Man-in-the-middle attacks.

Rather than leaving the new systems vulnerable by allowing connections from all clients in Computer Propertes > Remote settings, I discovered that Windows XP SP3 does in fact offer NLA support however it’s disabled by default. Somewhat frustratingly, the steps outlined in Microsoft KB 951608 require Registry edits which I would not want to encourage non-IT-savvy people to try. Giving out a .reg file is not really a good idea here either since these are additions to existing values, so forced replacements could interfere with certain vendors’ VPN clients etc.

Here’s a VBScript for the task which will only install on XP SP3 and will detect if the modifications have already been made. You could easily target it at a whole group of PCs by iterating through an array of hostnames.

'Enables Network Level Authentication on XP SP3 (disabled by default)
'which allows you to use the Remote Desktop Client 6.1 to connect to
'Windows 7 and Windows Server 2008 R2 without degrading security

Option Explicit

Const HKEY_LOCAL_MACHINE = &H80000002

Dim strLsaKey, strLsaValue, strHostname, size, arrMultiRegSZ, objReg, objWMI, colItems, i, found, modified
Dim objItem, SPlevel, strOSVer, strSecProvKey, strSecProvValue, strValue

strLsaKey = "SYSTEM\CurrentControlSet\Control\Lsa"
strLsaValue = "Security Packages"
strSecProvKey = "SYSTEM\CurrentControlSet\Control\SecurityProviders"
strSecProvValue = "SecurityProviders"
strHostname = "."
modified = false
found = false

Set objWMI = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strHostname & "\root\cimv2")
Set colItems = objWMI.ExecQuery("SELECT * FROM Win32_OperatingSystem")
For Each objItem In colItems
  strOSVer = objItem.Version
  SPlevel = objItem.ServicePackMajorVersion
Next
If Not Left(strOSVer,3) = "5.1" Then
  WScript.Echo "This script is only intended for Windows XP."
  WScript.Quit
End If
If Not SPlevel >= 3 Then
  WScript.Echo "Please install the latest Windows XP Service Pack from Windows Update."
  WScript.Quit
End If

Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strHostname & "\root\default:StdRegProv")
objReg.GetMultiStringValue HKEY_LOCAL_MACHINE, strLsaKey, strLsaValue, arrMultiRegSZ
size = Ubound(arrMultiRegSZ)
For i=0 to size
  If arrMultiRegSZ(i) = "tspkg" Then
    found = true
  End If
Next
If found Then
  WScript.Echo "tspkg already added to HKLM\SYSTEM\CurrentControlSet\Control\Lsa"
Else
  ReDim Preserve arrMultiRegSZ(size + 1)
  arrMultiRegSZ(size + 1) = "tspkg"
  objReg.SetMultiStringValue HKEY_LOCAL_MACHINE, strLsaKey, strLsaValue, arrMultiRegSZ
  modified = true
End If

objReg.GetStringValue HKEY_LOCAL_MACHINE, strSecProvKey, strSecProvValue, strValue

If Instr(strValue,"credssp.dll") Then
  WScript.Echo "credssp.dll already added to HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders"
Else
  strValue = strValue & ", credssp.dll"
  objReg.SetStringValue HKEY_LOCAL_MACHINE, strSecProvKey, strSecProvValue, strValue
  modified = true
End If
If modified Then
  WScript.Echo "Settings updated. You will need to restart for the changes to become active."
End If

Set objReg = nothing
Set objWMI = nothing

Migrating roaming profiles to Windows 7

Though a Microsoft user migration tool exists, I only want the bare minimum to be carried across from XP and I definitely want automation to ensure consistency. I made this script to be run by support technicians while logged in on newly installed Windows 7 as the user being migrated. The My Documents folder redirection is particularly important – the script mounts the old XP profile’s registry hive from the server, and checks whether My Documents was correctly redirected to the home drive. If not, the script will migrate the files from the old location. This tidies up any old inconsistent user profiles. Having My Documents on the home drive keeps profile size down for faster logins when roaming and helps to reduce the chance of a user losing data if their hard disk fails.

I spent a while wondering how to keep this as a single script despite the need to use RunAs for the Registry hive mount before hitting on the idea of recursion – see highlight below (%0 is the running script’s full name with path, %~nx0 is just the filename and extension). Despite the /env in the command which is supposed to make the invoked process share the main user environment, only some things are in fact shared. %TEMP% is for instance, though environment variables are not. So I pass the username as a command line, and recover the results of the Registry parse from a temporary file.

UPDATE – Since the file copying can take a long while I have made the My Documents path checking the first task, a failure of which will quit the script. There is now a five second delay between mounting the Registry hive and querying it) which should stop the script having to be run twice.

@echo off
set FServer=YourServerHere
echo .:: User profile migration script for user %USERNAME%
echo.

if not "%1" == "" goto checkxpreg

echo .:: We need admin rights to mount and check the user's XP profile registry hive
runas /env /noprofile /user:%USERDOMAIN%\Administrator "%~nx0 %USERNAME%"

if exist %TEMP%\XPMyDocsPath.txt type %TEMP%\XPMyDocsPath.txt
if not exist %TEMP%\XPMyDocsPath.txt (
  echo .:: ERROR the XP profile registry hive has not mounted - run the script again!
  goto :eof
)

if exist %TEMP%\DocsNeedMoving.txt (
  echo.
  echo .:: Migrating content to H:\ ready for Windows 7
  xcopy /y /e /c "\\%FSERVER%\profiles$\%USERNAME%\My Documents\*.*" "H:\"
)
if not exist %TEMP%\DocsNeedMoving.txt (
  echo.
  echo .:: No documents need moving
)
echo.

echo .:: My Documents redirection to root of H: drive
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" /v Personal /t REG_SZ /d "H:" /f
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" /v Personal /t REG_EXPAND_SZ /d %%HOMEDRIVE%% /f
echo.

echo .:: IE Favorites
xcopy /y /e /c \\%FSERVER%\profiles$\%USERNAME%\Favorites\*.* %USERPROFILE%\Favorites\
echo.

echo .:: Outlook signatures
xcopy /y /e /c "\\%FSERVER%\profiles$\%USERNAME%\Application Data\Microsoft\Signatures\*.*" %USERPROFILE%\AppData\Roaming\Microsoft\Signatures\

echo .:: Outlook nicknames file
if not exist %USERPROFILE%\AppData\Roaming\Microsoft\Outlook\*.* md %USERPROFILE%\AppData\Roaming\Microsoft\Outlook
copy /y "\\%FSERVER%\profiles$\%USERNAME%\Application Data\Microsoft\Outlook\*.nk2" %USERPROFILE%\AppData\Roaming\Microsoft\Outlook\
echo.

echo .:: Word user templates
xcopy /y /e /c "\\%FSERVER%\profiles$\%USERNAME%\Application Data\Microsoft\Templates\*.*" %USERPROFILE%\AppData\Roaming\Microsoft\Templates\
echo.

echo .:: Desktop files and shortcuts
xcopy /y /e /c \\%FSERVER%\profiles$\%USERNAME%\Desktop\*.* %USERPROFILE%\Desktop\
echo.

:: Stop the shell from changing the display name of the folder to My Documents on the fileserver
if exist H:\Desktop.ini (
  attrib -r -s -h H:\Desktop.ini
  del H:\Desktop.ini
)
:: Cleanup
if exist %TEMP%\DocsNeedMoving.txt del %TEMP%\DocsNeedMoving.txt
del %TEMP%\XPMyDocsPath.txt

pause
goto :eof

:checkxpreg
if exist del %%TEMP%\DocsNeedMoving.txt
reg load "HKEY_USERS\TempXPRegHive" "\\%FSERVER%\profiles$\%1\NTUSER.DAT"
echo .:: Waiting 5 seconds - Registry hive mount seems unreliable if queried immediately
ping 1.1.1.1 -n 1 -w 5000 > nul
for /f "tokens=2* delims= " %%a in ('reg query "HKEY_USERS\TempXPRegHive\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" /v Personal') do set TempMyDocs=%%b
echo XP profile My Documents=%TEMPMYDOCS%> %TEMP%\XPMyDocsPath.txt
echo %TEMPMYDOCS% | find /I "%1" && date /t> %TEMP%\DocsNeedMoving.txt
reg unload "HKEY_USERS\TempXPRegHive"
set TempMyDocs=

Customizing Windows 7 unattend.xml

Windows 7, like Vista, uses an XML answer file to configure the OS install. What’s neat about this is that even though you use the WAIK‘s WSIM tool to edit and validate it, you can customize it and add your own sections for software packages etc. as you can see from the example below, though these custom sections will need to be inserted after the sections that WSIM validates. This answer file can easily be parsed with VBScript using MSXML DOM, allowing for variables like passwords, driver sets, product keys and so on to be inserted at build time.

Why use an unattended install?

If you’ve always used an unattended install to build your workstations, you’ll know that they can be extremely versatile. If you already have a scripted build for XP with applications then chances are you’ll want to tweak those scripts to work with Windows 7. Sure, Sysprep images are handy too, but unless all your machines are the same, or all your packaged app requirements are identical, then you’ll need to add a load of scripted customization to them anyway. Which begs the question: why not just use an unattended install? That way you eliminate application problems that sometimes surface after image-based deployment. In a previous job I remember Roxio deployment in particular was a nightmare for this reason.

Microsoft have certainly made things considerably easier with the release of the Microsoft Deployment Toolkit 2010 but, though it offers a great introduction into the process of automated system building, it lacks the flexibility of rolling your own build process – in particular if you already have a host database. When I decide to rebuild a machine, it boots Windows PE from WDS, looks up its MAC address in the host database and reads the model type from WMI and will offer default choices in my build script menus based on that. It also works out which is the nearest site file server to use for the install.

The problem with Sysprep deployment if you have a very mixed hardware environment is that you either have to:

  • Create a WIM image for each different hardware type (lots of boring maintenance when changes are required)
  • Add a huge bulk of drivers to a single WIM image with DISM
  • Use the AuditSystem phase to connect to a driver share and re-detect all the hardware

Since Windows Vista and later versions effectively just install a WIM and run a hardware detection phase during their normal install process, Sysprep no longer offers much of a speed improvement over an unattended build.

Device drivers

Additional mass storage and networking drivers that will be essential during setup are detected in the Windows PE instance (which we booted from WDS) Driver Store and ‘reflected’ into the installed OS. As you build your custom Windows PE boot image, add these using DSIM as described in my post on the subject.

Since we can read the PC model name from the BIOS via WMI, we can add a tailored device path to the offlineServicing phase of setup. This allows for easy maintenance of driver bundles, since we can arrange them by model type (which certainly beats having dozens of SoundMAX audio drivers lumped together), and we can limit which drivers are offered to each model – particularly useful when a driver causes problems for some models, as with the GMA950 driver and Adobe Flash on Intel 945G motherboards.

I noticed that this is in fact considerably neater that the method used in MDT2010′s ZTIDrivers.wsf script, which copies drivers to the local system then invokes the AuditSystem phase, running the PnP detection routines a second time, slowing down the install. Useful for an OEM like Dell I suppose, whose PCs are often started from their factory image with no connectivity, but not ideal for corporate LANs.

Sample autounattend.xml with custom sections at the end

Note 1 – I couldn’t find any examples of this online, but I discovered that the values for pre-populating the Internet Explorer 8 Search Providers can be obtained by configuring a workstation, then harvesting the registry settings from HKCU\Software\Microsoft\Internet Explorer\SearchScopes. I have highlighted the relevant lines in the XML below.

Note 2 – Somewhat confusingly, in the offlineServicing phase Microsoft-Windows-PnpCustomizationsNonWinPE will fail to connect to your file server for drivers unless you connect to its FQDN (assuming the unattended launched the OS build from a share referencing just the NetBIOS name). Fail to do this and %systemroot%\panther\setupact.log will reveal that it fails to connect with error 0x4C3 (multiple credentials on connection to the same server). What’s bizarre is that there certainly aren’t multiple credentials in use – I use the same ones throughout. I wrote up this problem and solution in this thread on the MSFN Forums. I suspect this might be caused because I launch setup.exe from a network drive rather than mounting the OS WIM image from a WDS server (I wanted to maintain consistency with my other legacy OS builds). I have highlighted this on line 57.

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="windowsPE">
        <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SetupUILanguage>
                <UILanguage>en-US</UILanguage>
            </SetupUILanguage>
            <InputLocale>0809:00000809</InputLocale>
            <SystemLocale>en-GB</SystemLocale>
            <UILanguage>en-US</UILanguage>
            <UserLocale>en-GB</UserLocale>
        </component>
        <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ComplianceCheck>
                <DisplayReport>Never</DisplayReport>
            </ComplianceCheck>
            <Diagnostics>
                <OptIn>false</OptIn>
            </Diagnostics>
            <DiskConfiguration>
                <WillShowUI>Always</WillShowUI>
            </DiskConfiguration>
            <DynamicUpdate>
                <Enable>true</Enable>
                <WillShowUI>OnError</WillShowUI>
            </DynamicUpdate>
            <ImageInstall>
                <OSImage>
                    <InstallFrom>
                        <MetaData wcm:action="add">
                            <Key>/IMAGE/NAME</Key>
                            <Value>Windows 7 PROFESSIONAL</Value>
                        </MetaData>
                    </InstallFrom>
                </OSImage>
            </ImageInstall>
            <UserData>
                <AcceptEula>true</AcceptEula>
                <FullName>IT</FullName>
                <Organization>My company</Organization>
                <ProductKey>
                    <WillShowUI>OnError</WillShowUI>
                </ProductKey>
            </UserData>
            <EnableNetwork>true</EnableNetwork>
        </component>
    </settings>
    <settings pass="offlineServicing">
        <component name="Microsoft-Windows-PnpCustomizationsNonWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <DriverPaths>
                <PathAndCredentials wcm:action="add" wcm:keyValue="common">
                    <Credentials>
                        <Domain>domain.com</Domain>
                        <Password>Password</Password>
                        <Username>unattended</Username>
                    </Credentials>
                    <Path>\\myserver.domain.com\UNATTENDED\drivers\7-x64\common</Path>
                </PathAndCredentials>
                <PathAndCredentials wcm:action="add" wcm:keyValue="build">
                    <Credentials>
                        <Domain>*value to be set by install.vbs*</Domain>
                        <Password>*value to be set by install.vbs*</Password>
                        <Username>*value to be set by install.vbs*</Username>
                    </Credentials>
                    <Path>*value to be set by install.vbs*</Path>
                </PathAndCredentials>
            </DriverPaths>
        </component>
    </settings>
    <settings pass="generalize">
        <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <PersistAllDeviceInstalls>true</PersistAllDeviceInstalls>
        </component>
    </settings>
    <settings pass="specialize">
        <component name="Microsoft-Windows-IE-InternetExplorer" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <Home_Page>http://mycompany.com</Home_Page>
            <CompanyName>My Company</CompanyName>
            <FavoritesOnTop>true</FavoritesOnTop>
            <FilterLevel>High</FilterLevel>
            <Help_Page></Help_Page>
            <DisableFirstRunWizard>true</DisableFirstRunWizard>
            <DisableWelcomePage>true</DisableWelcomePage>
            <PlaySound>true</PlaySound>
            <ShowInformationBar>true</ShowInformationBar>
            <UserAgent></UserAgent>
            <Window_Title_CN></Window_Title_CN>
            <SearchScopes>
                <Scope wcm:action="add">
                    <ScopeDefault>true</ScopeDefault>
                    <ScopeKey>Search1</ScopeKey>
                    <ScopeDisplayName>Google</ScopeDisplayName>
                    <ScopeUrl>http://www.google.com/search?q={searchTerms}&amp;sourceid=ie7&amp;rls=com.microsoft:{language}:{referrer:source}&amp;ie={inputEncoding?}&amp;oe={outputEncoding?}</ScopeUrl>
                    <FaviconURL>http://www.google.com/favicon.ico</FaviconURL>
                    <SuggestionsURL>http://clients5.google.com/complete/search?q={searchTerms}&amp;client=ie8&amp;mw={ie:maxWidth}&amp;sh={ie:sectionHeight}&amp;rh={ie:rowHeight}&amp;inputencoding={inputEncoding}&amp;outputencoding={outputEncoding}</SuggestionsURL>
                </Scope>
                <Scope wcm:action="add">
                    <ScopeKey>Search2</ScopeKey>
                    <ScopeDisplayName>Bing</ScopeDisplayName>
                    <FaviconURL>http://www.bing.com/favicon.ico</FaviconURL>
                    <ScopeUrl>http://www.bing.com/search?q={searchTerms}&amp;form=IE8SRC&amp;src=IE-SearchBox</ScopeUrl>
                    <SuggestionsURL>http://api.bing.com/qsml.aspx?query={searchTerms}&amp;market={Language}&amp;form=IE8SSC&amp;maxwidth={ie:maxWidth}&amp;rowheight={ie:rowHeight}&amp;sectionHeight={ie:sectionHeight}</SuggestionsURL>
                </Scope>
            </SearchScopes>
            <EnableLinksBar>false</EnableLinksBar>
            <PrintBackground>true</PrintBackground>
        </component>
        <component name="Microsoft-Windows-RemoteAssistance-Exe" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <fAllowFullControl>true</fAllowFullControl>
            <fAllowToGetHelp>true</fAllowToGetHelp>
        </component>
        <component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SkipAutoActivation>true</SkipAutoActivation>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ComputerName>*</ComputerName>
            <ProductKey>XXXXX-YYYYY-ZZZZZ-YYYYY-XXXXX</ProductKey>
            <RegisteredOrganization>My Company</RegisteredOrganization>
            <RegisteredOwner>IT</RegisteredOwner>
        </component>
        <component name="Microsoft-Windows-TerminalServices-LocalSessionManager" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <fDenyTSConnections>false</fDenyTSConnections>
        </component>
        <component name="Microsoft-Windows-UnattendedJoin" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <Identification>
                <Credentials>
                    <Domain>*value to be set by install.vbs*</Domain>
                    <Password>*value to be set by install.vbs*</Password>
                    <Username>*value to be set by install.vbs*</Username>
                </Credentials>
                <JoinDomain>domain.com</JoinDomain>
                <MachineObjectOU>OU=Windows 7,OU=Workstations,DC=domain,DC=com</MachineObjectOU>
            </Identification>
        </component>
        <component name="Networking-MPSSVC-Svc" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <DomainProfile_EnableFirewall>false</DomainProfile_EnableFirewall>
        </component>
        <component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <UserAuthentication>1</UserAuthentication>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SystemLocale>en-GB</SystemLocale>
            <UILanguage>en-GB</UILanguage>
            <UserLocale>0809:00000809</UserLocale>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <RegisteredOrganization>My Company</RegisteredOrganization>
            <RegisteredOwner>IT</RegisteredOwner>
            <TimeZone>GMT Standard Time</TimeZone>
            <OEMInformation>
                <Manufacturer>*</Manufacturer>
                <Model>*</Model>
            </OEMInformation>
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>1</ProtectYourPC>
            </OOBE>
            <UserAccounts>
                <AdministratorPassword>
                    <Value>MwBifhgftytredjghAcwB0AHIAYQ584jkhkgtEAcwBzAHcAbwByAGQA</Value>
                    <PlainText>false</PlainText>
                </AdministratorPassword>
                <LocalAccounts>
                    <LocalAccount wcm:action="add">
                        <Password>
                            <Value>QQBTAEUANABoAGchgfhgfd357wrysAHMAcwB3AG8AcgBkAA==</Value>
                            <PlainText>false</PlainText>
                        </Password>
                        <Name>cust_localuser</Name>
                        <DisplayName>cust_localuser</DisplayName>
                        <Description>Dummy user required for unattended - delete later</Description>
                        <Group>users</Group>
                    </LocalAccount>
                </LocalAccounts>
            </UserAccounts>
            <VisualEffects>
                <FontSmoothing>ClearType</FontSmoothing>
            </VisualEffects>
            <FirstLogonCommands>
                <SynchronousCommand wcm:action="add">
                    <Order>1</Order>
                    <Description>Connect to unattended share</Description>
                    <CommandLine>net use *value to be set by install.vbs*</CommandLine>
                </SynchronousCommand>
                <SynchronousCommand wcm:action="add">
                    <Order>2</Order>
                    <Description>Launch package installer</Description>
                    <CommandLine>cscript *value to be set by install.vbs*</CommandLine>
                    <RequiresUserInput>true</RequiresUserInput>
                </SynchronousCommand>
            </FirstLogonCommands>
            <AutoLogon>
                <Password>
                    <Value>MwBifhgftytredjghAcwB0AHIAYQ584jkhkgtEAcwBzAHcAbwByAGQA</Value>
                    <PlainText>false</PlainText>
                </Password>
                <Username>Administrator</Username>
                <LogonCount>1</LogonCount>
                <Enabled>true</Enabled>
            </AutoLogon>
        </component>
    </settings>
    <cpi:offlineImage cpi:source="wim://myserver/unattended/os/7-x64/sources/install.wim#Windows 7 PROFESSIONAL" xmlns:cpi="urn:schemas-microsoft-com:cpi" />
    <!-- MY COMPANY CUSTOMIZATIONS BELOW-->
    <mycompany:custom xmlns:mycompany="urn:schemas-domain-com:mycompany">
        <!-- build description -->
        <cust_description>standard workstation</cust_description>
        <!-- list of packages to install in order. Descriptions are read from U:\scripts\packages.csv -->
        <cust_packages>office2k7.cmd,adobe.cmd,flash.cmd,java.cmd,fonts.cmd</cust_packages>
        <!-- start of the model name string, as reported by WMI, comma separated - allows auto-selection of build. -->
        <cust_models>OptiPlex 745,OptiPlex GX620,VMware Virtual Platform</cust_models>
        <!-- do we want packages.vbs to activate Windows? -->
        <cust_activate>false</cust_activate>
        <!-- do we want a hibernation file wasting disk space? If set to false packages.vbs will disable hibernation support -->
        <cust_hibernate>false</cust_hibernate>
    </mycompany:custom>
</unattend>
 

Sample VBScript code for parsing the XML using MSXML DOM

This is not a complete script – it’s intended purely to illustrate the concept.

'New OS - 7/Vista/2008/2008R2

'read the answer file
Set objXML = CreateObject("Microsoft.XMLDOM")
objXML.Async = "False"
objXML.Load(strMedia & "\sifs\" & strOS & "\" & strBuild & ".xml")

'Additional mass storage and Networking drivers that will be essential for the rest of the build are detected in the
'running Windows PE instance's Driver Store and reflected into the installed OS

'insert the non-WinPE PnP driver discovery paths and credentials for the offlineServicing pass
strXPath = "/unattend/settings[@pass='offlineServicing']/component[@name='Microsoft-Windows-PnpCustomizationsNonWinPE']/DriverPaths/PathAndCredentials/Credentials"
'we need to iterate since there may be several sets of credentials (one for each driver path)
Set colNodes=objXML.selectNodes(strXPath & "/Domain")
For Each objNode In colNodes
  objNode.Text = strDomainName
Next
Set colNodes=objXML.selectNodes(strXPath & "/Password")
For Each objNode In colNodes
  objNode.Text = strPass
Next
Set colNodes=objXML.selectNodes(strXPath & "/Username")
For Each objNode In colNodes
  objNode.Text = strUser  
Next

'Insert hostname
strXPath = "/unattend/settings[@pass='specialize']/component[@name='Microsoft-Windows-Shell-Setup']/ComputerName"
Set objNode=objXML.selectSingleNode(strXPath)
objNode.Text = strComputerName

'insert credentials for Domain join
On Error Resume Next
strXPath = "/unattend/settings[@pass='specialize']/component[@name='Microsoft-Windows-UnattendedJoin']/Identification/Credentials"
Set objNode=objXML.selectSingleNode(strXPath & "/Domain")
If Err.Number = 0 Then
  'Microsoft-Windows-UnattendedJoin exists - carry on modifying (workgroup builds won't have this section in the XML)
  objNode.Text = strDomainName
  Set objNode=objXML.selectSingleNode(strXPath & "/Password")
  objNode.Text = strPass
  Set objNode=objXML.selectSingleNode(strXPath & "/Username") 
  objNode.Text = strUser
End If
On Error Goto 0

'insert manufacturer and model into OEMinfo
strXPath = "/unattend/settings[@pass='oobeSystem']/component[@name='Microsoft-Windows-Shell-Setup']/OEMInformation/Manufacturer"
Set objNode=objXML.selectSingleNode(strXPath)
objNode.Text = strManufacturer
strXPath = "/unattend/settings[@pass='oobeSystem']/component[@name='Microsoft-Windows-Shell-Setup']/OEMInformation/Model"
Set objNode=objXML.selectSingleNode(strXPath)
objNode.Text = strModel & " - Your Company"

strXPath = "/unattend/settings[@pass='oobeSystem']/component[@name='Microsoft-Windows-Shell-Setup']/FirstLogonCommands/SynchronousCommand/CommandLine"
Set colNodes=objXML.selectNodes(strXPath)
For Each objNode In ColNodes
  'Insert the unattended share info and credentials
  If InStr (objNode.Text,"net use *value to be set by install.vbs*") Then
    objNode.Text = "net use " & strMedia & " \\" & strInstallServer & "\" & strShare & " /user:" & strUser & "@" & strDomainName & " " & strPass & " /persistent:no"
  End If
  'Record our build file selection for the package installer later
  If InStr (objNode.Text,"cscript *value to be set by install.vbs*") Then
    objNode.Text = "cscript //nologo " & strMedia & "\scripts\packages.vbs " & strMedia & " " & strOS & " " & strBuild & ".xml"
  End If
Next

Set objNode = Nothing
'write out the answer file
objXML.Save "x:\autounattend.xml" 
Set objXML = Nothing

Simple batch script for file renaming

Ever ended up with 100 files you need to rename? Like removing  ” copy” from each filename after a batch Photoshop action? Or removing ” (small)” after resizing a big folder of images using the Image Resizer Powertoy? When you start looking online for a tool to do this most of them are commercial software, even for something so outwardly simple.

Here’s something I wrote a while ago for a colleague who runs Photoshop actions. It seemed like an easy challenge but the script actually took a while to figure out, mainly because I was determined that it should call no additional programs. I managed in the end using a couple of neat tricks: delayed variable expansion, and the little-known string replace function of the Set command. I also allowed drag & drop for the target folder containing the files you want renaming.

The script below targets .jpg files but I’ve highlighted the lines you would need to edit to change its behaviour:

::simple batch file renamer
::
::EXPLANATION - delayed variable expansion must be enabled since we want to
::expand the vars on each pass, not just once as the whole FOR loop is parsed.
::The SET command includes some simple string substitution methods (see SET /?)
::Below, I am setting newname=oldname with " (small)" substituted to ""
::The script displays what it's about to do before it does it and also supports
::dragging and dropping of the target folder.
::
@echo off
setlocal ENABLEDELAYEDEXPANSION
set folderpath=
if "%~1"=="" (
  echo No target folder was dragged ^& dropped - using local directory.
) else (
  echo Opening folder %1
  set folderpath=%1\
)
echo.
call :rename echo
echo.
echo Proceed with rename? (CTRL-C to cancel).
pause
call :rename
goto :eof

:rename
for %%i in (%folderpath%*.jpg) do (
  set oldname=%%~ni%%~xi
  ::to replace " (Small)" with ""
  set newname=!oldname: ^(Small^)=!
  %1 ren "%%~fi" "!newname!"
)

Changing IP settings fast

When I need to keep changing TCP/IP settings to test networking configs it’s a real pain to have to keep opening up the adapter properties (especially on Windows 7) and usually I’m in too much of a hurry to lookup up the netsh command syntax. For that reason I’m posting this small script. Save it as ipset.cmd.

Note: using netsh to revert to DHCP seems to be intermittent if no DHCP server is available – e.g. if the adapter has no link.

Windows 7

@echo off
If "%1" == "" (
  echo Configures Local Area Connection
  echo   ipset address/maskbits gateway (dns)
  echo   ipset 192.168.1.99/24 192.168.1.254
  echo   ipset 192.168.1.99/24 192.168.1.254 8.8.8.8
  echo   ipset dhcp
  echo.
  goto :eof
)
If "%1" == "dhcp" (
  netsh interface ip set dnsservers name="Local Area Connection" source=%1
  netsh interface ip set address name="Local Area Connection" source=%1
  goto :eof
)
netsh interface ip set address name="Local Area Connection" source=static address=%1 gateway=%2
If "%3" == "" (
  :: OpenDNS public DNS servers
  netsh interface ip set dnsservers name="Local Area Connection" source=static address=208.67.222.222
  netsh interface ip add dnsservers name="Local Area Connection" address=208.67.222.220
) else (
  netsh interface ip set dnsservers name="Local Area Connection" source=static address=%3
)

Windows XP

@echo off
If "%1" == "" (
  echo Configures Local Area Connection
  echo   ipset address mask gateway
  echo   ipset 192.168.1.99 255.255.255.0 192.168.1.254
  echo   ipset dhcp
  echo.
  goto :eof
)
If "%1" == "dhcp" (
  netsh int ip set address local source=%1
  netsh int ip set dns local source=%1
  goto :eof
) else (
  netsh int ip set address local static %1 %2 %3 1
  :: OpenDNS public DNS servers
  netsh int ip add dns local 208.67.222.222 index=1
  netsh int ip add dns local 208.67.222.220 index=2
)

Moving your SQL 2005 databases ready for VSS off-host backups

Many storage vendors now offer hardware Volume ShadowCopy Service providers for their storage arrays which allow the SAN itself to carry out the snapshot, rather than the underlying OS. These providers are Exchange- and SQL-aware so they will quiesce the transaction logs just before the snapshot.

The big win here is off-host backup – the target server asks the SAN to snap the data volume then carries on as normal. The backup media server meanwhile will mount this SAN snapshot and back it up directly from the SAN. In this way you can backup SQL and Exchange environments in the middle of the day without any performance degradation (assuming of course that you have the IOPS headroom on the SAN). Symantec Backup Exec 12.5 and later supports this technology but it must be purchased as an option – Advanced Disk-based Backup Option or ADBO.

However, to off-host backup Exchange or SQL you will need to have both the databases/mail stores and the transaction logs on the same SAN LUN. This flies in the face of the old wisdom of segregating logs onto RAID1 spindles, but it’s important to realise that a modern SAN makes this perfectly viable. The EqualLogic PS4000XV in my environment for instance has a write latency of <1.0ms in RAID50. Microsoft used to recommend keeping SQL logs on disk with a write latency sub 10ms (now they say 1-5ms).

Moving all of the transaction logs on a crowded SQL server is tricky for several reasons:

  • the Transact-SQL database alter command requires you to know the database’s logical filenames. On an SQL 2005 server, these are largely predictable but it gets complicated when some of the DBs have migrated from SQL 2000 and if some of them were restored from backups of databases with different names (dev or test versions which then went into production for example).
  • the system databases are likely to be on C: and if you want to grab all DBs in one backup job these will need moving too (TempDB is usually ignored by most backup software and can stay where it is).
  • though some guides on the Web suggest detaching and re-attaching the DBs – this is a surefire way to end up with a total disaster since the re-attached DBs will have new GUIDs which will wreck SharePoint amongst others.

Backing up then restoring your databases specifying new file paths is one method but the danger is that you would need to isolate them so that no changes occurred during that time window (which could be a long time).

Moving user databases

The best solution for these is 99% careful preparation work – to build a long list of T-SQL database alter commands to change the SQL file references, and a batch script to move the actual files to the database drive. You can also use this as an opportunity to clean up any badly named files, and move ones that are in the wrong place.

It is highly recommended that if you haven’t already done so, you should set the following registry values on the SQL server which will guard against future inconsistencies. If they already exist, check they’re still valid:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL.1\MSSQLServer\DefaultData
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL.1\MSSQLServer\DefaultLog
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL.1\MSSQLServer\BackupDirectory
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL.1\MSSQLServer\FullTextDefaultPath

For each database you need to run the following Transact-SQL:

Use [DBname]
Select * from sys.database_files

This will return all the files in the filegroup including full text catalogs (if they exist) together with their logical names (name column):
Database logical filenames

In this example the transaction log is already in the desired location, but it if was in say C:\TRANSACTIONS LOGS we would need to write:

alter database [SUSDB] modify file (name = SUSDB_log, filename = 'G:\DATABASES\SUSDB_log.ldf')

You would then add this to your file move batch script:

move /y "C:\TRANSACTION LOGS\SUSDB_log.LDF" G:\DATABASES\SUSDB_log.ldf

My method was to run a full SQL backup to commit the transaction logs (less data to move), run the alter database commands all at once (which don’t take effect until the SQL Server service next starts), stop the SQL Server service, run the file move batch script, check for any errors, then start the SQL Server service again. Once it’s up, you can try to expand each database in SQL Management Studio. Any databases with damaged file paths will not expand. Refer back to your command prompt window to try and figure out what went wrong (usually a typo).

In this way you should be able to move all of the logs with a bare minimum of downtime – several minutes in my case.

Moving system databases

Moving system databases is fairly straightforward, but it will require a little more downtime. Again, I’d probably leave TempDB where it is to separate its I/O from the rest as it can be high and we don’t need to back it up. If you do want to move it, the procedure is the same as any non-system database. The rest though are special cases.

Run the following and note the current file locations which will probably be in C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\DATA

use model
select * from sys.database_files
use msdb
select * from sys.database_files
use master
select * from sys.database_files

Now close SQL Management Studio and run the following from a Command Prompt (the parameters are case sensitive!):

net stop mssqlserver
net start mssqlserver /c /m /T3608

Open SQL Management Studio again but read this carefully. With these startup parameters, SQL Server will only allow a single connection. The default behaviour of the GUI is to open the Object Explorer window once you connect, which counts as a connection. You need to click on the Disconnect button, and close the Object Explorer child window. You should then be able to open a New Query.
If you closed the Object Explorer without disconnecting you’ll get the error “Server is in single user mode. Only one administrator can connect at this time.” and you’ll need to stop and start the service again, as above, and repeat. Next:

sp_detach_db 'model'
sp_detach_db 'msdb'

Move the files to the new location (logs and databases remember), then run the following taking care to substitute in your new file paths:

sp_attach_db 'model','G:\DATABASES\model.mdf','G:\DATABASES\modellog.ldf'
sp_attach_db 'msdb','G:\DATABASES\msdbdata.mdf','G:\DATABASES\msdblog.ldf'

Stop the SQL Server service. Start it again normally (no parameters) and check you can expand model and msdb in Management Studio.

We just have the master database left to move. Stop the SQL Server service again. Move master’s log and database files to the new location. On the SQL server machine’s console, open Start Menu > Programs > Microsoft SQL Server 2005 > Configuration Tools > SQL Server Configuration Manager.
In the category SQL 2005 Services, select SQL Server (MSSQLSERVER) and look at the Properties. Select the Advanced tab. Select Startup Parameters and pull down the dropdown next to it.
Change the value from the defaults of:

-dC:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\DATA\master.mdf;-eC:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\LOG\ERRORLOG;-lC:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\DATA\mastlog.ldf

to your new file paths (don’t change the error log path by accident):

-dG:\DATABASES\master.mdf;-eC:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\LOG\ERRORLOG;-lG:\DATABASES\mastlog.ldf

SQL Server Configuration Manager
Finally, start the SQL Server Service. Done!

The trouble with full-text catalogs

If you rely on the EqualLogic Auto-Snaphot Manager to tell you whether your databases now support SAN snapshots you could be in for a surprise when you backup using ADBO in Backup Exec:

V-79-57344-34086 – ADBO: Offhost backup initialization failure on: “myhostname.domain.com”.
Snapshot provider error (0xE0008526): Backup Exec could not locate a Microsoft Volume Shadow Copy Services (VSS) software or hardware snapshot provider for this job. Select a valid VSS snapshot provider for the target computer.
Check the Windows Event Viewer for details.

This is an awful error message because it doesn’t really describe the problem (and you won’t find anything meaningful in the Event Viewer). It almost looks like a registration failure of the Hardware VSS Provider, which is misleading, and caused me about 2 hours of out-of-hours work reinstalling it, taking the server offline, etc. to satisfy Symantec Support. However, run a job with the same selection list but using normal AOFO (Advanced Open File Backup) and you get:

AOFO: Initialization failure on: “myhostname.domain.com”. Advanced Open File Option used: Microsoft Volume Shadow Copy Service (VSS).
V-79-10000-11219 – VSS Snapshot error. The volume or snapped volume may not be available or may not exist. Check the configuration of the snapshot provider, and then run the job again.
The following volumes are dependent on resource: “C:” “D:” “G:”.

Much clearer – there’s a dependency on the D: drive being detected, the drive I migrated from. By chance I changed the backup selection list realised that some databases backed up while others didn’t. The cause turned out to be a full text catalog.

The EqualLogic ASM only checks the database and log files, not full-text catalogs. Moving these seems to be pretty difficult. Microsoft have an MSDN document describing database moves (see section on catalogs further down the page). I have tried following this process to the letter, and when that didn’t work I tried various permutations of stopping the SQL Server service, the SQL FullText Search service (which seems to autorestart), the SQL Server Agent service, copying the files, not copying the files (expecting SQL to move them) etc. No combination seemed to work for me. What I found was that, while it is easy enough to move the catalog path like so:

alter database [ExampleDB] modify file (name = [sysft_ExampleDB], filename = 'G:\DATABASES\FTData\ExampleDB')

there is some meta data that does not get updated and the ADBO backup will still fail when the VSS provider checks all the file dependencies.
sys.database_files shows the correct paths. Eventually I discovered that

Select * from sys.fulltext_catalogs

still showed the old location for the catalogs. The only way I could find to get this to update was to rebuild the full-text catalog in SQL Management Studio - expand the database > Storage > Full Text Catalogs > right-click > Rebuild.

For me this was acceptable and quick, but I imagine some infrastructures might not be so tolerant of a rebuild.

Outlook stationery – scripting default font and HTML signature

Many organizations decide to use a house style for email, and being able to force a default font is the only reliable way to get this consistent. I do this with the login script.

What’s difficult about it is that it’s not just a minor registry edit – the font styles are actually written in HTML and then stored in binary in the registry. The best way to get them modified is to use an Outlook client to make the changes, then grab them from the registry. They’re stored at:

HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Common\MailSettings

In Outlook you need to edit the stationery in Tools > Options > Mail Format tab > Stationery and Fonts, making sure to change the font for both new messages and replies.

The registry values which are modified are:

  • ComposeFontComplex
  • ComposeFontSimple
  • ReplyFontComplex
  • ReplyFontSimple

Here’s what my ComposeFontComplex looks like converted back to text:

<html>

<head>
<style>

 /* Style Definitions */
 span.PersonalComposeStyle
	{mso-style-name:"Personal Compose Style";
	mso-style-type:personal-compose;
	mso-style-noshow:yes;
	mso-style-unhide:no;
	mso-ansi-font-size:10.0pt;
	mso-bidi-font-size:11.0pt;
	font-family:"Verdana","sans-serif";
	mso-ascii-font-family:Verdana;
	mso-hansi-font-family:Verdana;
	mso-bidi-font-family:"Times New Roman";
	mso-bidi-theme-font:minor-bidi;
	color:windowtext;}
-->
</style>
</head>

</html>

The cleanest way to script this is to export the values from regedit (they will be in hex) and insert them into the script unmodified with the line breaks intact. Then you just quote and line-wrap them, split them into an array on the ”,” character and use a hex-to-binary function before inserting them into the user registry. Here is my example OutlookStationery subroutine for reference  (note that it’s not a working script – some objects are defined elsewhere, e.g. objReg, the Active Directory job title, name, surname etc.):

Sub OutlookStationery
  'Standardized Outlook signature based on AD data
  WScript.Echo "Updating Outlook signature and default font"
  Dim objWord, objDoc, objSelection, objEmailOptions, objSignatureObject, objSignatureEntries, objLink
  Set objWord = CreateObject("Word.Application")
  Set objDoc = objWord.Documents.Add()
  Set objSelection = objWord.Selection
  Set objEmailOptions = objWord.EmailOptions
  Set objSignatureObject = objEmailOptions.EmailSignature
  Set objSignatureEntries = objSignatureObject.EmailSignatureEntries
  objSelection.TypeText strFirstName & " " & strSurname
  objSelection.TypeText(Chr(11))
  objSelection.TypeText strTitle
  objSelection.TypeText(Chr(11))
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Tel +xx xxx xxxx " & strExtension
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Fax +xx xxx xxxx xxxx"
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Web "
  Set objLink = objSelection.Hyperlinks.Add(objSelection.Range,"http://mycompanywebsite.com/",,"My Company Website","companywebsite.com")
  objSelection.TypeText(Chr(11))
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "The Company Name"
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Address Line 1"
  objSelection.TypeText(Chr(11))
  objSelection.TypeText "Address City"
  objSelection.TypeText(Chr(11))
  objSelection.TypeText(Chr(11))
  objselection.Font.Bold = True
  objSelection.TypeText "Company"
  objselection.Font.Bold = False
  objSelection.TypeText " strapline goes here"
  Set objSelection = objDoc.Range()
  objSelection.Font.Name = "Verdana"
  objSelection.Font.Size = "10"
  objSignatureEntries.Add "Standard Signature", objSelection
  objSignatureObject.NewMessageSignature = "Standard Signature"
  objSignatureObject.ReplyMessageSignature = ""
  objDoc.Saved = True
  objWord.Quit

  'Force the default font for Outlook messages. The hex arrays are captures from a Regedit export, line breaks intact for easy later amendment
  Dim arrComposeFontComplexHex, arrComposeFontComplex, arrReplyFontComplexHex, arrReplyFontComplex
  Dim arrComposeFontSimpleHex, arrComposeFontSimple, arrReplyFontSimpleHex, arrReplyFontSimple

  arrComposeFontComplexHex = Split ("3c,68,74,6d,6c,3e,0d,0a,0d,0a,3c,68,65,61,64,3e,0d,0a," &_
   "3c,73,74,79,6c,65,3e,0d,0a,0d,0a,20,2f,2a,20,53,74,79,6c,65,20,44,65,66,69," &_
   "6e,69,74,69,6f,6e,73,20,2a,2f,0d,0a,20,73,70,61,6e,2e,50,65,72,73,6f,6e,61," &_
   "6c,43,6f,6d,70,6f,73,65,53,74,79,6c,65,0d,0a,09,7b,6d,73,6f,2d,73,74,79,6c," &_
   "65,2d,6e,61,6d,65,3a,22,50,65,72,73,6f,6e,61,6c,20,43,6f,6d,70,6f,73,65,20," &_
   "53,74,79,6c,65,22,3b,0d,0a,09,6d,73,6f,2d,73,74,79,6c,65,2d,74,79,70,65,3a," &_
   "70,65,72,73,6f,6e,61,6c,2d,63,6f,6d,70,6f,73,65,3b,0d,0a,09,6d,73,6f,2d,73," &_
   "74,79,6c,65,2d,6e,6f,73,68,6f,77,3a,79,65,73,3b,0d,0a,09,6d,73,6f,2d,73,74," &_
   "79,6c,65,2d,75,6e,68,69,64,65,3a,6e,6f,3b,0d,0a,09,6d,73,6f,2d,61,6e,73,69," &_
   "2d,66,6f,6e,74,2d,73,69,7a,65,3a,31,30,2e,30,70,74,3b,0d,0a,09,6d,73,6f,2d," &_
   "62,69,64,69,2d,66,6f,6e,74,2d,73,69,7a,65,3a,31,31,2e,30,70,74,3b,0d,0a,09," &_
   "66,6f,6e,74,2d,66,61,6d,69,6c,79,3a,22,56,65,72,64,61,6e,61,22,2c,22,73,61," &_
   "6e,73,2d,73,65,72,69,66,22,3b,0d,0a,09,6d,73,6f,2d,61,73,63,69,69,2d,66,6f," &_
   "6e,74,2d,66,61,6d,69,6c,79,3a,56,65,72,64,61,6e,61,3b,0d,0a,09,6d,73,6f,2d," &_
   "68,61,6e,73,69,2d,66,6f,6e,74,2d,66,61,6d,69,6c,79,3a,56,65,72,64,61,6e,61," &_
   "3b,0d,0a,09,6d,73,6f,2d,62,69,64,69,2d,66,6f,6e,74,2d,66,61,6d,69,6c,79,3a," &_
   "22,54,69,6d,65,73,20,4e,65,77,20,52,6f,6d,61,6e,22,3b,0d,0a,09,6d,73,6f,2d," &_
   "62,69,64,69,2d,74,68,65,6d,65,2d,66,6f,6e,74,3a,6d,69,6e,6f,72,2d,62,69,64," &_
   "69,3b,0d,0a,09,63,6f,6c,6f,72,3a,77,69,6e,64,6f,77,74,65,78,74,3b,7d,0d,0a," &_
   "2d,2d,3e,0d,0a,3c,2f,73,74,79,6c,65,3e,0d,0a,3c,2f,68,65,61,64,3e,0d,0a,0d," &_
   "0a,3c,2f,68,74,6d,6c,3e,0d,0a", ",")

  arrReplyFontComplexHex = Split ("3c,68,74,6d,6c,3e,0d,0a,0d,0a,3c,68,65,61,64,3e,0d,0a," &_
   "3c,73,74,79,6c,65,3e,0d,0a,0d,0a,20,2f,2a,20,53,74,79,6c,65,20,44,65,66,69," &_
   "6e,69,74,69,6f,6e,73,20,2a,2f,0d,0a,20,73,70,61,6e,2e,50,65,72,73,6f,6e,61," &_
   "6c,52,65,70,6c,79,53,74,79,6c,65,0d,0a,09,7b,6d,73,6f,2d,73,74,79,6c,65,2d," &_
   "6e,61,6d,65,3a,22,50,65,72,73,6f,6e,61,6c,20,52,65,70,6c,79,20,53,74,79,6c," &_
   "65,22,3b,0d,0a,09,6d,73,6f,2d,73,74,79,6c,65,2d,74,79,70,65,3a,70,65,72,73," &_
   "6f,6e,61,6c,2d,72,65,70,6c,79,3b,0d,0a,09,6d,73,6f,2d,73,74,79,6c,65,2d,6e," &_
   "6f,73,68,6f,77,3a,79,65,73,3b,0d,0a,09,6d,73,6f,2d,73,74,79,6c,65,2d,75,6e," &_
   "68,69,64,65,3a,6e,6f,3b,0d,0a,09,6d,73,6f,2d,61,6e,73,69,2d,66,6f,6e,74,2d," &_
   "73,69,7a,65,3a,31,30,2e,30,70,74,3b,0d,0a,09,6d,73,6f,2d,62,69,64,69,2d,66," &_
   "6f,6e,74,2d,73,69,7a,65,3a,31,31,2e,30,70,74,3b,0d,0a,09,66,6f,6e,74,2d,66," &_
   "61,6d,69,6c,79,3a,22,56,65,72,64,61,6e,61,22,2c,22,73,61,6e,73,2d,73,65,72," &_
   "69,66,22,3b,0d,0a,09,6d,73,6f,2d,61,73,63,69,69,2d,66,6f,6e,74,2d,66,61,6d," &_
   "69,6c,79,3a,56,65,72,64,61,6e,61,3b,0d,0a,09,6d,73,6f,2d,68,61,6e,73,69,2d," &_
   "66,6f,6e,74,2d,66,61,6d,69,6c,79,3a,56,65,72,64,61,6e,61,3b,0d,0a,09,6d,73," &_
   "6f,2d,62,69,64,69,2d,66,6f,6e,74,2d,66,61,6d,69,6c,79,3a,22,54,69,6d,65,73," &_
   "20,4e,65,77,20,52,6f,6d,61,6e,22,3b,0d,0a,09,6d,73,6f,2d,62,69,64,69,2d,74," &_
   "68,65,6d,65,2d,66,6f,6e,74,3a,6d,69,6e,6f,72,2d,62,69,64,69,3b,0d,0a,09,63," &_
   "6f,6c,6f,72,3a,23,31,46,34,39,37,44,3b,0d,0a,09,6d,73,6f,2d,74,68,65,6d,65," &_
   "63,6f,6c,6f,72,3a,64,61,72,6b,32,3b,7d,0d,0a,2d,2d,3e,0d,0a,3c,2f,73,74,79," &_
   "6c,65,3e,0d,0a,3c,2f,68,65,61,64,3e,0d,0a,0d,0a,3c,2f,68,74,6d,6c,3e,0d,0a", ",")

  arrComposeFontSimpleHex = Split ("3c,00,00,00,1f,00,00,f8,00,00,00,40,c8,00,00,00,00,00," &_
   "00,00,00,00,00,ff,00,22,56,65,72,64,61,6e,61,00,00,00,00,00,00,00,00,00,00," &_
   "00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00", ",")

  arrReplyFontSimpleHex = Split ("3c,00,00,00,1f,00,00,f8,00,00,00,00,c8,00,00,00,00,00,00," &_
   "00,1f,49,7d,00,00,22,56,65,72,64,61,6e,61,00,00,00,00,00,00,00,00,00,00,00," &_
   "00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00", ",")

  arrComposeFontComplex = ArrayHexToDec(arrComposeFontComplexHex)
  arrReplyFontComplex = ArrayHexToDec(arrReplyFontComplexHex)
  arrComposeFontSimple = ArrayHexToDec(arrComposeFontSimpleHex)
  arrReplyFontSimple = ArrayHexToDec(arrReplyFontSimpleHex)

  objReg.SetBinaryValue HKEY_CURRENT_USER,"Software\Microsoft\Office\12.0\Common\MailSettings", "ComposeFontComplex", arrComposeFontComplex
  objReg.SetBinaryValue HKEY_CURRENT_USER,"Software\Microsoft\Office\12.0\Common\MailSettings", "ReplyFontComplex", arrReplyFontComplex
  objReg.SetBinaryValue HKEY_CURRENT_USER,"Software\Microsoft\Office\12.0\Common\MailSettings", "ComposeFontSimple", arrComposeFontSimple
  objReg.SetBinaryValue HKEY_CURRENT_USER,"Software\Microsoft\Office\12.0\Common\MailSettings", "ReplyFontSimple", arrReplyFontSimple
End Sub

Function ArrayHexToDec(arrHex)
  Dim i, arrDec
  ReDim arrDec(UBound(arrHex))
  For i = 0 to UBound(arrHex)
    If arrHex(i) = "00" Then
      arrDec(i) = 0
    Else
      arrDec(i) = CByte("&H" & arrHex(i))
    End If
  Next
  ArrayHexToDec = arrDec
End Function