Category Archives: Scripting

PowerShell for EAP-PEAP secured VPN on Windows 8.1

Simple VPN configurations can be deployed by Group Policy but EAP authentication settings cannot be configured like this, even using Windows 8.1 and Windows Server 2012 R2. Microsoft added some new PowerShell cmdlets to Windows 8.1 for configuring VPNs, but the worked examples do not appear to function for all the settings for PEAP connections, and they do not show a worked example of how you go about exporting and re-importing a connection’s XMLStream.

Defining the XML as a block within the script itself, even assigning it as data type XML does not seem to work. Not being particularly accustomed to PowerShell, the following script took a while to get right. I assigned it as a laptop startup script by GPO. If I need to modify the connection in future I can increment the version number since the script checks the local machine Registry for that, and will not install if the desired version marker is already present.

 
# VPN Connection EAP-PEAP VPN provisioning 
# patters 2013

# This script is loosely based on the EAP-TTLS one published by Microsoft at http://technet.microsoft.com/en-us/library/jj613766.aspx
# The worked examples on that page and at http://technet.microsoft.com/en-us/library/jj554822.aspx
# are rudimentary, and in some details for PEAP, incorrect. To set advanced options like the TrustedRootCAs and the
# the server identity checking warning, you *must* export a GUI-authored config as XML. Configuring XML attributes alone
# will not work because some of them are missing when creating a new connection, and adding them results in errors.


# Check for marker in the Registry, and quit if found
# Desired version is 1
$version = 1
$test = Get-ItemProperty "HKLM:\Software\MyCompany" "MyCompany VPN" -ErrorAction SilentlyContinue
If ($test -eq $null) {
       $test = 0
} else {
       $test = $test."MyCompany VPN"
}
If ($test -ge $version) {exit} 

# VPN Connection look-up to remove any previous installations
$isTestVpn = $false
$vpnConnections = Get-VpnConnection -AllUserConnection
If($vpnConnections.Name -eq "MyCompany VPN") {Remove-VpnConnection -Name "MyCompany VPN" -AllUserConnection -Confirm:$false -Force}
$vpnConnections = Get-VpnConnection
If($vpnConnections.Name -eq "MyCompany VPN") {Remove-VpnConnection -Name "MyCompany VPN" -Confirm:$false -Force}

Try
{
       #-------------------------------------------------
       #The following section documents the attempts to get this working manually before I got importing/exporting of XML working

       # http://technet.microsoft.com/en-us/library/jj554822.aspx says to use "New-EapConfiguration -Peap" here, but is wrong      
       #$a = New-EapConfiguration

       # Generate configuration XML for PEAP authentication method with EAP-MSCHAPv2 as its inner method
       #$b = New-EapConfiguration -Peap -VerifyServerIdentity -FastReconnect $true -TunnledEapAuthMethod $a.EapConfigXmlStream

       # Edit properties within the generated configuration XML
       #$c = $b.EapConfigXmlStream
       #$c.EapHostConfig.Config.Eap.EapType.ServerValidation.ServerNames = "vpn.mycompany.com"

       # Specify AddTrust Root CA for Comodo - This attribute is missing unless you create the connection using the GUI
       # The following appears to generate the XML correctly, but it won't be accepted by the Add-VpnConnection cmdlet
       #$c.EapHostConfig.Config.Eap.EapType.ServerValidation.SetAttribute("TrustedRootCA","02 fa f3 e2 91 43 54 68 60 78 57 69 4d f5 e4 5b 68 85 18 68")   

       # PeapExtensions settings are nested XML objects so setting them as string datatype will fail
       # see http://www.vistax64.com/powershell/173859-xml-property-text.html
       #$c.EapHostConfig.Config.Eap.EapType.PeapExtensions.PerformServerValidation."#text" = "true"
       #$c.EapHostConfig.Config.Eap.EapType.PeapExtensions.AcceptServerName."#text" = "true"
       # Once again this attribute is missing unless the connection is created using the GUI. Adding it does not work
       #$c.EapHostConfig.Config.Eap.EapType.PeapExtensions.PeapExtensionsV2.AllowPromptingWhenServerCANotFound."#text" = "true"      

       # Create the VPN connection ‘MyCompany VPN’ with the EAP configuration XML generated above
       #Add-VpnConnection -Name "MyCompany VPN" -ServerAddress "vpn.mycompany.com" -TunnelType Sstp -EncryptionLevel Maximum -AuthenticationMethod Eap -EapConfigXmlStream $c -AllUserConnection
       #-------------------------------------------------



       # FORTUNATELY THERE IS AN EASIER WAY (once you figure out PowerShell XML – why couldn’t MS have shown a worked example in the docs)...



       # Create your VPN configuration entry manually then export its XML like so:
       #$exportXML = (Get-VpnConnection -Name "My_VPN_Final" -AllUserConnection).EapConfigXmlStream
       #$exportXML.Save("${env:temp}\My_VPN_config.xml")

       $importXML = New-Object XML
       $importXML.Load("\\mycompany.com\data\Software\MyCompany VPN\MyCompany VPN.xml")
       Add-VpnConnection -Name "MyCompany VPN" -ServerAddress "vpn.mycompany.com" -TunnelType Sstp -EncryptionLevel Maximum -AuthenticationMethod Eap -EapConfigXmlStream $importXML -AllUserConnection
       
       # Leave a marker in the Registry
       If (-Not (Test-Path "HKLM:\Software\MyCompany")) {New-Item -Path "HKLM:\Software\MyCompany"}
       if (Get-ItemProperty "HKLM:\Software\MyCompany" "MyCompany VPN" -ErrorAction SilentlyContinue) {
              Set-ItemProperty -Path "HKLM:\Software\MyCompany" -Name "MyCompany VPN" -Value $version
       } else {
              New-ItemProperty -Path "HKLM:\Software\MyCompany" -Name "MyCompany VPN" -Value $version
       }

}
Catch
{
       Write-Host "Error in connection setup!"
       Write-Host $_.Exception.Message
       Throw
}

Tunlr enable/disable script for Microsoft Surface

I recently bought a Microsoft Surface and I have been wanting to watch a few programmes on BBC iPlayer whilst out of the country for Christmas. I discovered Tunlr – a free media proxy service which allows access to Hulu and iPlayer regardless of geolocation. However, editing DNS server settings by hand is time consuming and awkward without using the trackpad, so I wanted a script to automate the task. This will also work for other proxy services such as unblock-us.com – just replace the DNS IPs in the script. I had previously written a quick script for changing IP configuration which used netsh commands but these don’t work on Windows RT. Some other PowerShell methods I found weren’t supported either but I did find new network settings cmdlets for the purpose that were added in Windows 8/RT.

The next problem was elevation to get sufficient rights to change the network settings. It transpires that the PowerShell and VBScript environments are heavily restricted in Windows RT, which prevents auto-prompting for elevation. Fortunately Windows RT does allow Run as Administrator from the right-click menu for .cmd scripts. If you’re using touch control, you just touch and hold then release for the right-click. The script will remind you if you forget to do this. Hover your mouse over the top right corner of the script below, and use the View Source button to save the following to your desktop as Tunlr.cmd:

@echo off

::Use Tunlr to watch streaming TV services regardless of geolocation
::Tunlr DNS servers redirect requests for well-known services via Tunlr's proxy servers
::Tunlr should only be used while watching streams to reduce server load
::More details at http://tunlr.net/

::Elevation cannot be automated on Windows RT since object creation is disabled for PowerShell and VBScript
::http://stackoverflow.com/questions/13504499/windows-rt-powershell-permissiondenied-on-new-object

ipconfig /all | find "142.54.177.158" > nul && (
  echo Disabling Tunlr...
  PowerShell -Command Set-DnsClientServerAddress -InterfaceAlias "WiFi" -ResetServerAddresses || (
    echo Right-click and re-run this script as Administrator
    pause
  )
) || (
  echo Enabling Tunlr...
  PowerShell -Command Set-DnsClientServerAddress -InterfaceAlias "WiFi" -ServerAddresses 142.54.177.158,209.141.56.79 || (
    echo Right-click and re-run this script as Administrator
    pause
  )
)

UAC elevation for Windows batch script

I recently needed to make an interactive batch script elevate for admin privileges. I found an example script by jagaroth, and then refined it to make it even more compact. It only writes out one temporary script file, and passes the rest of the required variables on the command line. It can cope with paths containing spaces. It was something of a shell escaping nightmare as you can see from line 14!

@echo off

::Windows XP doesn't have UAC so skip
for /f "tokens=3*" %%i in ('reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v ProductName ^| Find "ProductName"') do set WINVER=%%i %%j 
echo %WINVER% | find "XP" > nul && goto commands

::prompt for elevation
if "%1" == "UAC" goto elevation
(
  echo Set objShell = CreateObject^("Shell.Application"^)
  echo Set objFSO = CreateObject^("Scripting.FileSystemObject"^)
  echo strPath = objFSO.GetParentFolderName^(WScript.ScriptFullName^)
  echo If objFSO.FileExists^("%~0"^) Then
  echo   objShell.ShellExecute "cmd.exe", "/c """"%~0"" UAC ""%~dp0""""", "", "runas", 1
  echo Else
  echo   MsgBox "Script file not found"
  echo End If
) > "%TEMP%\UAC.vbs"
cscript //nologo "%TEMP%\UAC.vbs"
goto :eof
:elevation
del /q "%TEMP%\UAC.vbs"

:commands
::navigate back to this script's home folder
%~d2
cd "%~p2"

::put your main script here
echo 1st arg: %1
echo 2nd arg: %2
pause

Unified Windows PE 4.0 builder for Windows ADK

This script will build Windows PE 4.0 (for x86, or AMD64 or both) including scripts and drivers of your choosing, it will create ISO images with both BIOS and UEFI support, and will also upload the resulting WIM boot images to your WDS server automatically (and freshen them if they have been re-created). This reduces the tiresome task of boot image maintenance to just a couple of clicks.

It uses only the standard Microsoft Windows ADK tools, which is the new name for WAIK. Just save the code below as Build_WinPE.cmd and right-click on it to Run as Administrator. Notice the defined variables at the start, particularly the %SOURCE% folder. It supports using either the 32bit or the 64bit ADK, and only the Windows PE and Deployment Tools ADK components are required. The script expects the following folders:

  • %SOURCE%\scripts\WinPE – any additional scripts (e.g. OS build scripts)
  • %SOURCE%\drivers\WinPE-x86\CURRENT – drivers
  • %SOURCE%\drivers\WinPE-AMD64\CURRENT
  • %SOURCE%\tools\WinPE-x86 – optional tools such as GImageX, or apps from portableapps.com
  • %SOURCE%\tools\WinPE-AMD64

Notice the optional components section at lines 90-95. Modify this if you need your image to contain additional items, for instance PowerShell or .NET Framework 4.

One further observation is that Macs don’t seem to be able to boot this version of Windows PE. I’m not sure whether this is a GOP display driver issue, or whether only true UEFI firmwares are required (Macs are EFI which is an earlier specification). To carry out an unattended Windows 8 install on a Mac via BootCamp you will need to build a Windows PE 3.0 ISO since Macs can’t PXE boot.

There’s some more info about UEFI booting on 32bit architectures here – apparently UEFI 2.3.1 compliance is a requirement. My VAIO’s Insyde H2O UEFI firmware certainly seems to ignore EFI loaders.

:: Build_WinPE.cmd
::
:: patters 2012
::
:: This script will build x86 and AMD64 Windows PE 4.0, automatically
:: collecting drivers from the relevant folders within the
:: unattended installation, building WIM and ISO images, and
:: will also upload the WIM images to the deployment server(s).
::
:: DO NOT cancel this script in progress as you can end up with
:: orphaned locks on files inside mounted WIM images which
:: usually require a reboot of the server to clear.
::

@echo off
setlocal ENABLEDELAYEDEXPANSION

::variables
     set SOURCE=\\WDSSERVER\unattended
     set PE_TEMP=C:\temp
     ::WinPE feature pack locale
     set PL=en-US
     ::commma separated list for WDS_SERVERS
     set WDS_SERVERS=WDSSERVER1,WDSSERVER2
::end variables

if "%PROCESSOR_ARCHITECTURE%"=="x86" set PRGFILES32=%PROGRAMFILES%
if "%PROCESSOR_ARCHITECTURE%"=="AMD64" set PRGFILES32=%PROGRAMFILES(X86)%

if not exist "%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\*.*" (
     echo This script requires the Windows Assessment and Deployment Kit to be installed
     echo Download it from http://www.microsoft.com/en-us/download/details.aspx?id=30652
     echo.
     pause
     goto :eof
)
if "%1"=="relaunch" (
     call :BUILD_WINPE %2 %3 %4
     goto :eof
)
if "%1"=="unmount" (
     :: use this if you have a problem with the script and there are WIMs still mounted
     dism /Unmount-Wim /MountDir:"%PE_TEMP%\WinPE-x86\mount" /discard
     dism /Unmount-Wim /MountDir:"%PE_TEMP%\WinPE-AMD64\mount" /discard
     goto :eof
)
:prompt
cls
set /P SELECTION=Build WinPE for which CPU architecture (AMD64, x86, both)? [AMD64]: 
if "%SELECTION%"=="" set SELECTION=AMD64
if "%SELECTION%"=="amd64" set SELECTION=AMD64
if "%SELECTION%"=="X86" set SELECTION=x86
if "%SELECTION%"=="b" set SELECTION=both
if "%SELECTION%"=="BOTH" set SELECTION=both
if "%SELECTION%"=="AMD64" (
     start "Building Windows PE for AMD64 - NEVER CANCEL THIS SCRIPT IN PROGRESS" cmd /c "%0" relaunch AMD64
     goto :eof
)
if "%SELECTION%"=="x86" (
     start "Building Windows PE for x86 - NEVER CANCEL THIS SCRIPT IN PROGRESS" cmd /c "%0" relaunch x86
     goto :eof
)
if "%SELECTION%"=="both" (
     ::opening both instances of this script simultaneously seems to cause race conditions with dism.exe
     start /wait "Building Windows PE for x86 - NEVER CANCEL THIS SCRIPT IN PROGRESS" cmd /c "%0" relaunch x86 nopause
     start "Building Windows PE for AMD64 - NEVER CANCEL THIS SCRIPT IN PROGRESS" cmd /c "%0" relaunch AMD64
     goto :eof
)
goto :prompt

:BUILD_WINPE
set PE_ARCH=%1
set OSCDImgRoot=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\Oscdimg
set WinPERoot=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Windows Preinstallation Environment
set DandIRoot=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools
set DISMRoot=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\DISM
set PATH=%PATH%;%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\Oscdimg
set PATH=%PATH%;%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\BCDBoot
set PATH=%PATH%;%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PROCESSOR_ARCHITECTURE%\DISM
set PATH=%PATH%;%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Windows Preinstallation Environment
echo on
rd /s /q %PE_TEMP%\WinPE-%PE_ARCH%
call copype.cmd %PE_ARCH% %PE_TEMP%\WinPE-%PE_ARCH%
::package path
set PP=%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Windows Preinstallation Environment\%PE_ARCH%\WinPE_OCs
::image path
set IP=%PE_TEMP%\WinPE-%PE_ARCH%\mount
echo on
dism /Mount-Wim /WimFile:"%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot.wim" /Index:1 /MountDir:"%IP%"
dism /image:"%IP%" /Add-Package /PackagePath:"%PP%\WinPE-Scripting.cab"^
 /PackagePath:"%PP%\%PL%\WinPE-Scripting_%PL%.cab" /PackagePath:"%PP%\WinPE-WMI.cab"^
 /PackagePath:"%PP%\%PL%\WinPE-WMI_%PL%.cab" /PackagePath:"%PP%\WinPE-MDAC.cab"^
 /PackagePath:"%PP%\%PL%\WinPE-MDAC_%PL%.cab" /PackagePath:"%PP%\WinPE-HTA.cab"^
 /PackagePath:"%PP%\%PL%\WinPE-HTA_%PL%.cab" /PackagePath:"%PP%\WinPE-Dot3Svc.cab"^
 /PackagePath:"%PP%\%PL%\WinPE-Dot3Svc_%PL%.cab"
dism /image:"%IP%" /Add-Driver /driver:"%SOURCE%\drivers\WinPE-%PE_ARCH%\CURRENT" /Recurse
copy "%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PE_ARCH%\BCDBoot\bootsect.exe" "%IP%\Windows"
copy /y "%SOURCE%\scripts\WinPE\*.*" "%IP%\Windows\System32"
copy "%SOURCE%\tools\WinPE-%PE_ARCH%\*.*" "%IP%\Windows\System32"
copy /y "%PRGFILES32%\Windows Kits\8.0\Assessment and Deployment Kit\Deployment Tools\%PE_ARCH%\DISM\imagex.exe" "%IP%\Windows\System32"
dism /Unmount-Wim /MountDir:"%IP%" /commit

::Mac OS BootCamp will look for autorun.inf in order to validate this disk as a Windows Installer CD
::adding this allows us to start unattended installs using WinPE
date /T > "%PE_TEMP%\WinPE-%PE_ARCH%\media\autorun.inf"

::bootable ISO includes both BIOS & EFI boot loaders
oscdimg -m -o -u2 -udfver102 -bootdata:2#p0,e,b"%PE_TEMP%\WinPE-%PE_ARCH%\fwfiles\etfsboot.com"#pEF,e,b"%PE_TEMP%\WinPE-%PE_ARCH%\fwfiles\efisys.bin" "%PE_TEMP%\WinPE-%PE_ARCH%\media" "%PE_TEMP%\WinPE-%PE_ARCH%\WinPE-40-%PE_ARCH%.iso"
@echo off

::rename the WIM file to avoid having multiple image files on the WDS server with the same filename
ren "%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot.wim" boot_%PE_ARCH%.wim

if "%PE_ARCH%"=="x86" set WDS_ARCH=%PE_ARCH%
if "%PE_ARCH%"=="AMD64" set WDS_ARCH=X64
for %%i in (%WDS_SERVERS%) do (
     echo.
     echo Adding/updating boot image on WDS server: %%i
     :: try to add the image first, if that fails then replace existing
     wdsutil /Verbose /Progress /Add-Image /ImageFile:"%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot-40-%PE_ARCH%.wim"^
      /Server:%%i /ImageType:Boot /Name:"Microsoft Windows PE 4.0 (%PE_ARCH%)" || wdsutil /Verbose /Progress /Replace-Image^
      /Image:"Microsoft Windows PE 4.0 (%PE_ARCH%)" /ImageType:Boot /Architecture:%WDS_ARCH% /ReplacementImage^
      /Name:"Microsoft Windows PE 4.0 (%PE_ARCH%)" /ImageFile:"%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot-40-%PE_ARCH%.wim"^
      /Server:%%i
     echo.
)
::rename the WIM back again so bootable USB devices can be created
ren "%PE_TEMP%\WinPE-%PE_ARCH%\media\sources\boot-40-%PE_ARCH%.wim" boot.wim
echo *******************************************************************
echo WDS boot image(s) updated
echo.
echo A bootable ISO of this image has been created at:
echo   %PE_TEMP%\WinPE-%PE_ARCH%\WinPE-40-%PE_ARCH%.iso
echo.
echo To create a bootable USB key, use diskpart.exe to create a FAT32 partition
echo and mark it active, then copy the contents of this folder to its root:
echo   %PE_TEMP%\WinPE-%PE_ARCH%\media
echo.
echo FAT32 is required for EFI support.
echo.
if "%2"=="nopause" goto :eof
pause
goto :eof

Windows software deployment and update script

For many years I have used scripts of my own design to build workstations and to roll out software updates. At the time I created these I found that most of the tools which could accomplish these tasks were unwieldy. Group Policy software deployment in particular never really seemed fit for purpose since it extended login times so dramatically. My experience gained in a previous job spent packaging applications for deployment had taught me that all installed software populates consistent information in the Windows Registry, so in my current job I tended to audit this data directly via my scripts. This was saved into an SQL database from where it could be queried, or manipulated via a data source in Excel.

I’m working my notice period at the moment ready for a new job I’ll start in October, and so I’m going over the stuff I have created in the current job in order to prepare my handover documents. Mindful of the dependency my current employer has on these custom scripts I decided to get a quote for a Dell KACE solution, thinking that since it’s a Virtual Appliance, and since there are only 150 PCs here it shouldn’t be too expensive – after all it’s only really providing what my scripts already do (workstation builds, drivers, software deployment, and auditing). But here’s the thing – they wanted something like £13,000! (I can’t recall the precise figure). To put it in context this figure is around one third of the cost of replacing all the workstations with new ones, or say half the annual salary of an IT support technician – quite out of the question.

Unsurprisingly I have decided instead to simply tidy up my scripts to make them easier to use. Sure, you could accomplish these tasks with SCCM but that’s not free either. In an SME, why spend huge amounts of money on something that can be automated without much trouble using mechanisms that are built in. Heck, even the uninstall command line is stored in the registry for virtually all software – that’s how the Add/Remove Programs Control Panel works! And most software can be installed silently in the desired way provided you research the command line arguments to do so. It’s no accident that AppDeploy.com which was a great crowdsourced repository of this knowledge became KACE which was then acquired by Dell. It still exists, though the content doesn’t seem to be as well maintained as it was.

I have used a startup script written in VBScript to keep software up to date on workstations. A startup script runs as the SYSTEM account so permissions are not an issue. Since I also maintain an unattended installation I already have a package folder with all the scripts to install each package. All I needed to code was a way to audit the Registry for each package and add some logic around that. Up until now, I had tended to write sections of the script specifically tailored for each package, and from there it’s not much of a stretch to apply packages to a workstation based on its OS version, or Active Directory OU or group membership. For the script I have published below, I have recreated this logic as a single function which can be invoked with a one line entry for each package (see the highlighted part) – everything else is taken care of. I hope it helps someone to save £13,000 :)

 

Sample script output

Running software package check for Adobe Flash Player...
  Registry data found at branch "Adobe Flash Player ActiveX"
  Comparing detected version 11.3.300.271 against desired version 11.4.402.265
  Removing old version 11.3.300.271
    Killing iexplore.exe
    Override detected, running "u:\packages\flash\uninstall_flash_player.exe -uninstall"
    u:\packages\flash\uninstall_flash_player.exe -uninstall
  Installing Adobe Flash Player 11.4.402.265

Running software package check for Paint.NET...
  Registry data found at branch "{529125EF-E3AC-4B74-97E6-F688A7C0F1C0}"
  Comparing detected version 3.60.0 against desired version 3.60.0
  Paint.NET is already installed and up to date.

Running software package check for Adobe Reader...
  Registry data found at branch "{AC76BA86-7AD7-1033-7B44-AA0000000001}"
  Comparing detected version 10.0.0 against desired version 10.1.4
  Removing old version 10.0.0
    Using UninstallString from the Registry, plus "/qb-!"
    MsiExec.exe /I{AC76BA86-7AD7-1033-7B44-AA0000000001} /qb-!
  Installing Adobe Reader 10.1.4

Running software package check for Photo Gallery...
  Registry data found at branch "{60A1253C-2D51-4166-95C2-52E9CF4F8D64}"
  Comparing detected version 16.4.3503.0728 against desired version 16.4.3503.0728
  Photo Gallery is already installed and up to date.

Running software package check for Mendeley Desktop...
  Installing Mendeley Desktop 1.6
 

The script

'startup.vbs
'patters 2006-2012

Option Explicit
Dim objNetwork, objShell, objReg, strKey, colProcess, objProcess, arrSubKeys 
Dim strFileServer
Const HKEY_CURRENT_USER = &H80000001
Const HKEY_LOCAL_MACHINE = &H80000002

'set up objects
Set objNetwork = CreateObject("WScript.Network")
Set objShell = CreateObject("WScript.Shell")
Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")

strFileServer = "YOURSERVERHERE"
MapNetworkDrive "U:","unattended"

Package "flash.cmd", "Adobe Flash Player", "11.4.402.265", "u:\packages\flash\uninstall_flash_player.exe -uninstall", False, True, "iexplore.exe"    
Package "paintnet.cmd", "Paint.NET", "3.60.0", "/qb-!", False, False, "" 
Package "adobe.cmd", "Adobe Reader", "10.1.4","/qb-!",False, False, array("outlook.exe","iexplore")
Package "photogal.cmd", "Photo Gallery", "16.4.3503.0728", "/qb-!", False, False, "iexplore.exe"
Package "mendeley.cmd", "Mendeley Desktop", "1.6", "/S", True, False, "winword.exe"

objNetwork.RemoveNetworkDrive "U:", True, True
WScript.Echo VbCrLf & "Finished software checks"


Function Package(strPackageName, strTargetDisplayName, strTargetVersion, strExtraUninstParams, boolExtraUninstQuotes, boolUninstForceOverride, ProcessToKill)

  '=============================================================================

  'To understand this function you need to know that installed software packages
  'will populate keys below these branches of the Registry:
  '  HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
  '  HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
  '    (the latter for 32bit software on 64bit Windows)
  'This is the data that is mined when you look at Add/Remove Programs
  'in the Control Panel 

  'strPackageName is the package script on your package server (e.g. flash.cmd)

  'strTargetDisplayName can be a full or partial match of the Registry key
  'DisplayName (matches from the left)
  '  "Java(TM)" would match "Java(TM) 6 Update 5" and all other versions

  'strTargetVersion is the full version number from DisplayVersion in the Registry
  'Each decimal point of precision will be compared in turn.

  'If the Registry key DisplayVersion is not used by a package, the same number
  'of digits is parsed from the right hand side of the DisplayName string

  'strExtraUninstParams is used when you want to override the command line
  'specified by QuietUninstallString in the Registry, or for when that value is
  'missing for example, sometimes InnoSetup packages will specify the switch
  '/SILENT in QuietUninstallString, but you may need to override by appending
  '/VERYSILENT to the command line in UninstallString
  'If neither QuietUninstallString and UninstallString are present, the script
  'will use strExtraUninstParams as the full uninstall command line
  
  'Some packages define UninstallString as a long filename but forget to
  'surround it with quotes. You can correct this by setting
  'boolExtraUninstQuotes = True
  '   Package "mendeley.cmd", "Mendeley Desktop", "1.6", "/S", True, False, "winword.exe"

  'In some cases you may want to ignore the value of both QuietUninstallString
  'and UninstallString and override the command completely. To do this, set
  'boolUninstForceOverride to True
  '   Package "flash.cmd", "Adobe Flash Player", "11.4.402.265", "u:\packages\flash\uninstall_flash_player.exe -uninstall", False, True, "iexplore.exe"

  'Finally, ProcessToKill is a string or array containing the name(s) of any
  'running process(es) you need to kill, if plugins are being installed for Word
  'or Internet Explorer for instance.

  '=============================================================================

  Dim arrBranches, strBranch, boolRemoval, strActualDisplayName, strActualVersion
  Dim strQuietUninstall, strUninstall
  WScript.Echo VbCrLf & "Running software package check for " & strTargetDisplayName & "..."
  'we need to iterate through both the 32 and 64bit uninstall branches of the Registry
  arrBranches = Array("SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\", "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\")
  For Each strBranch In arrBranches
    'firstly, remove old version of package if it's present
    objReg.EnumKey HKEY_LOCAL_MACHINE, strBranch, arrSubKeys
    If IsArray(arrSubkeys) Then
      For Each strKey in arrSubkeys
        objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "DisplayName", strActualDisplayName
        If Left(strActualDisplayName, Len(strTargetDisplayName)) = strTargetDisplayName Then
          'we've found the target software package
          WScript.Echo "  Registry data found at branch """ & strKey & """"
          'is there a version string (not all software will have one)?
          objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "DisplayVersion", strActualVersion
          If Not IsNull(strActualVersion) Then
          Else
            'if there's no version string we'll try to grab the same number of chars from the right hand side of the DisplayName string  
            strActualVersion = Right(strActualDisplayName, Len(strTargetVersion))
          End If
          If (IsUpgradeNeeded (strActualVersion,strTargetVersion)) = True Then
            strQuietUninstall = ""
            WScript.Echo "  Removing old version " & strActualVersion
            KillProcess ProcessToKill
            'check the package's registry settings
            objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "UninstallString", strUninstall
            objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "QuietUninstallString", strQuietUninstall
            If Not strExtraUninstParams = "" Then
              'Extra parameters were sent to the function
              If boolUninstForceOverride = True Then
                'Entire uninstall command line was forced so use strExtraUninstParams, regardless of what's in the Registry
                WScript.Echo "    Override detected, running """ & strExtraUninstParams & """"
                WScript.Echo "    " & strExtraUninstParams
                WinExec strExtraUninstParams
              ElseIf Not IsNull(strUninstall) Then
                'use the basic UninstallString plus the additional parameters
                If boolExtraUninstQuotes = True Then
                  strUninstall = """" & strUninstall & """"
                End If
                strUninstall = strUninstall & " " & strExtraUninstParams
                WScript.Echo "    Using UninstallString from the Registry, plus """ & strExtraUninstParams & """"
                WScript.Echo "    " & strUninstall
                WinExec strUninstall
              Else
                'no UninstallString was found in the Registry, so assume that strExtraUninstParams is the full removal command line
                WScript.Echo "    No UninstallString found, running """ & strExtraUninstParams & """"
                WScript.Echo "    " & strExtraUninstParams
                WinExec strExtraUninstParams
              End If
            Else
              'No extra parameters were sent to the function
              'if there's already a value for QuietUninstallString then use that command line
              If Not IsNull(strQuietUninstall) Then
                WScript.Echo "    Using QuietUninstallString directly from the Registry"
                WScript.Echo "    " & strQuietUninstall
                WinExec strQuietUninstall
              ElseIf Not IsNull(strUninstall) Then
                'no QuietUninstallString was found, fall back to UninstallString
                If boolExtraUninstQuotes = True Then
                  strUninstall = """" & strUninstall & """"
                End If
                WScript.Echo "    Using UninstallString directly from the Registry"
                WScript.Echo "    " & strUninstall
                WinExec strUninstall
              Else
                WScript.Echo "    ERROR - this package doesn't seem to have any UninstallString defined - you'll need to send one to the Package function (see script source for details)"
                Exit Function
              End If
            End If
          Else
            'IsUpgradeNeeded (strActualVersion,strTargetVersion) is False
            'package was detected, but version is >= than the one specified
            WScript.Echo "  " & strTargetDisplayName & " is already installed and up to date."
            Exit Function
          End If
        End If
      Next
    End If
  Next
  'install package
  WScript.Echo "  Installing " & strTargetDisplayName & " " & strTargetVersion
  KillProcess ProcessToKill
  WinExec "U:\packages\" & strPackageName
End Function


Function IsUpgradeNeeded(strVerActual,strVerDesired)
  Dim arrActualVersion, arrDesiredVersion, i
  'Break software version down on decimal points
  arrActualVersion = split(strVerActual,".")
  arrDesiredVersion = split(strVerDesired,".")
  WScript.Echo "  Comparing detected version " & strVerActual & " against desired version " & strVerDesired
  'iterate, comparing each sub-version number starting from left
  For i = 0 To UBound(arrActualVersion)
    'WScript.Echo "  comparing digit... is " & arrActualVersion(i) & " less than " & arrDesiredVersion(i) 
    If arrActualVersion(i) < arrDesiredVersion(i) Then
      'installed version is out of date
      IsUpgradeNeeded = True
      Exit Function
    ElseIf arrActualVersion(i) > arrDesiredVersion(i) Then
      'installed version is newer
      IsUpgradeNeeded = False
      Exit Function     
    End If
  Next
  'thus far the version numbers are the same, but there may be additional
  'decimal points of precision in the desired version
  '  e.g. Adobe Reader 10.1.4 is newer than 10.1
  If UBound(arrDesiredVersion) > UBound(arrActualVersion) Then
    IsUpgradeNeeded = True
  Else
    IsUpgradeNeeded = False
  End If
End Function


Function MapNetworkDrive(strDriveLetter, strSharePath)
  On Error Resume Next
  'if the share name is not a UNC path, assume it's on the normal fileserver
  If Not Left(strSharePath,2) = "\\" Then
    strSharePath = "\\" & strFileServer & "\" & strSharePath
  End If
  If objFSO.DriveExists(strDriveLetter) Then
    objNetwork.RemoveNetworkDrive strDriveLetter, True, True
  End If
  objNetwork.MapNetworkDrive strDriveLetter, strSharePath
  If Err.Number <> 0 Then
    WScript.Echo "Error - " & Err.Description
    Err.Clear
  End If
  On Error Goto 0
End Function


Function WinExec(strExec)
  Dim objExec, eTime
  WinExec = True
  Set objExec = objShell.Exec(strExec)
  eTime = DateAdd("s", 120, Now)
  Do While objExec.Status = 0
    WScript.Sleep 1000
  Loop
End Function


Function KillProcess(Process)
  Dim strProcessElement
  If IsArray(Process) Then
    For Each strProcessElement in Process
      KillIndividualProcess(strProcessElement)
    Next
  ElseIf Not Process = "" Then
    KillIndividualProcess(Process)
  End If
End Function


Function KillIndividualProcess(strProcess)
  Dim colProcess, objProcess
  Set colProcess = objWMI.ExecQuery("Select * from Win32_Process")
  For Each objProcess in colProcess
    If LCase(objProcess.Name) = LCase(strProcess) Then
      WScript.Echo "    Killing " & strProcess
      'occasionally one parent process may kill all children leading to an object error
      'so disable error handling temporarily
      On Error Resume Next
      objProcess.Terminate()
      On Error Goto 0
    End If
  Next
End Function

CrashPlan PROe Server package for Synology NAS

CrashPlan is a popular online backup solution, with most people using it to protect their data in the Cloud. However, by licensing CrashPlan PROe server you can be that Cloud and act as the storage provider for other client machines running CrashPlan PROe.

I was recently contracted to implement this on Synology hardware for North Bay Technologies, an IT services company in San Francisco. Instead of undertaking a careful manual install that would be difficult to maintain in future, I decided to go one better – to build a package which integrates properly with Synology DSM. I then back-merged most of the changes into the existing CrashPlan client package scripts so everything is as consistent as possible. It was agreed that I would also publish this to the community, so here it is!

CrashPlan-PROe-dashboard

I should stress at this point that although this package could technically install on Synology products with ARM or QorIQ processors, you should only use this on a model with an Intel CPU. Ideally you should equip it with more than 1GB of RAM too, because the application requires 1GB all for itself. The package repo will not advertise it to ARM systems, because they have far too little available RAM.

As with the CrashPlan client packages I have made, I have been careful to comply with Code 42 Software’s stipulation that no one redistributes their work.

CrashPlan-PROe-server-info

 

Installation

  • This package is for Intel CPUs only. It will work on an unmodified NAS, no hacking or bootstrapping required.
  • More than 1GB of RAM is recommended.
  • You will need to install my Java SE for Embedded package first. Read the instructions on that page carefully too.
  • In the User Control Panel in DSM, enable the User Homes service.
  • Purchase your Master License Key and licences (or obtain a trial key) from crashplan.com, download the PROe Server installer for Linux, and save in the public shared folder on your NAS. You should have created this folder when you installed the Java package.
  • Install the CrashPlan PROe server Synology package directly from Package Center in DSM. In Settings -> Package Sources add my package repository URL which is http://packages.pcloadletter.co.uk.
 

Notes

  • The package expects the end user to have separately downloaded the CrashPlan PROe Server installer for Linux, and it presents them with the official EULA during installation.
  • For details of TCP ports used, to help you set up firewalling and/or port forwarding please consult the requirements document.
  • Once running, CrashPlan PROe server is configured by a web dashboard on https://yourNasIP:4285/console/ This link can be found in the package info screen in Package Center in Synology DSM when the package is running.
  • Full support documentation is available here.
  • The DSM Package Center upgrade functionality allows you to move between my package versions without losing settings or data, but if you’re moving to a new CrashPlan PROe Server version you will need to do that manually via the admin web app, using the Linux downloads (with file extension .upgrade) available from Code 42 Software. Depending on how up to date your version is, you may need to update incrementally through several versions. Before you apply a Code 42 update package, you should manually install the latest Synology package for the specific PROe Server version you’re currently runnning. This will ensure the update scripts are handled correctly. So for example if you’re upgrading from 3.5.4 to 3.5.5 you should manually install cpproeserver3.5.4-merged-020.spk over the top first. You can find up to date package versions for each PROe Server build here:
    cpproeserver3.2.1.2-merged-0020.spk
    cpproeserver3.3.0.2-merged-0020.spk
    cpproeserver3.3.0.3-merged-0020.spk
    cpproeserver3.3.0.4-merged-0020.spk
    cpproeserver3.4.1-merged-0020.spk
    cpproeserver3.4.1.5-merged-0020.spk
    cpproeserver3.5.1.1-merged-0020.spk
    cpproeserver3.5.3.2-merged-0020.spk
    cpproeserver3.5.4-merged-0020.spk
    cpproeserver3.5.5-merged-0020.spk
  • The engine daemon script checks the amount of system RAM and scales the Java heap size appropriately (up to the default maximum of 1024MB). This can be overridden in a persistent way if you are backing up very large backup sets by editing /volume1/@appstore/cpproeserver/syno_package.vars.
  • As with my other syno packages, this user account password is randomized when it is created using a perl script called passgen (I followed the example of the Transmission package). DSM Package Center runs as the root user so my script starts the package using an su command. This means that you can change the password yourself and CrashPlan will still work.
  • The default location for saving backup data is set to /volume1/cpproeserver (where /volume1 is you primary storage volume) to eliminate the chance of them being destroyed accidentally by uninstalling the package.
  • The package supports upgrading to future versions while preserving the machine identity, logs, login details, and cache.
  • The log which is displayed in the package’s Log tab is actually the activity history. If you’re trying to troubleshoot an issue you may need to use an SSH session to inspect the more detailed log files which are stored in /volume1/cpproeserver/log
  • I’m not sure if it works for the PROe products, but I would really appreciate it if you could use this affiliate link when purchasing your licences (you may need to browse to the PROe section of the website using the links in the footer of that page). If this package saves you the several days worth of work I put into making it, please also consider donating using the PayPal button on the right hand side of the page. Thanks!
 

Package scripts

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

installer.sh

#!/bin/sh

#--------CRASHPLAN PROe server installer script
#--------package maintained at pcloadletter.co.uk

DOWNLOAD_PATH="http://download.crashplan.com/installs/proserver/CP_VER"
DOWNLOAD_FILE="CrashPlanPROServer_CP_VER_Linux.tgz"
DOWNLOAD_URL="${DOWNLOAD_PATH}/${DOWNLOAD_FILE}"
TGZ_FILE="CrashPlanPROServer.tgz"
#remove file extension
DOWNLOAD_FILE="`echo ${DOWNLOAD_FILE} | sed -e 's/.tgz$//'`"
EXTRACTED_FOLDER="${DOWNLOAD_FILE}"
DAEMON_USER="`echo ${SYNOPKG_PKGNAME} | awk {'print tolower($_)'}`"
DAEMON_PASS="`openssl rand 12 -base64 2>/dev/null`"
DAEMON_ID="${SYNOPKG_PKGNAME} daemon user"
DAEMON_HOME="/var/services/homes/${DAEMON_USER}"
OPTDIR="${SYNOPKG_PKGDEST}/proserver/server"
VARS_FILE="${OPTDIR}/.install.vars"
ENGINE_SCRIPT="proserver"
SYNO_CPU_ARCH="`uname -m`"
[ "${SYNO_CPU_ARCH}" == "x86_64" ] && SYNO_CPU_ARCH="i686"
NATIVE_BINS_URL="http://packages.pcloadletter.co.uk/downloads/crashplan-native-${SYNO_CPU_ARCH}.tgz"   
NATIVE_BINS_FILE="`echo ${NATIVE_BINS_URL} | sed -r "s%^.*/(.*)%\1%"`"
INSTALL_FILES="${NATIVE_BINS_URL}"
TEMP_FOLDER="`find / -maxdepth 2 -name '@tmp' | head -n 1`"
#this is where the user data will go, so it persists after a package uninstall
MANIFEST_FOLDER="/`echo $TEMP_FOLDER | cut -f2 -d'/'`/${DAEMON_USER}"
VARLOGDIR=${MANIFEST_FOLDER}/log
LOG_FILE="${VARLOGDIR}/com_backup42_app.log.0"
UPGRADE_FILES=""
UPGRADE_FOLDERS="activemq-data conf db keys"

source /etc/profile
PUBLIC_FOLDER="`cat /usr/syno/etc/smb.conf | sed -r '/\/public$/!d;s/^.*path=(\/volume[0-9]{1,4}\/public).*$/\1/'`"

  
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
  
  if [ -z ${JAVA_HOME} ]; then
    echo "Java is not installed or not properly configured. JAVA_HOME is not defined. "
    echo "Download and install the Java Synology package from http://wp.me/pVshC-z5"
    exit 1
  fi
  
  if [ ! -f ${JAVA_HOME}/bin/java ]; then
    echo "Java is not installed or not properly configured. The Java binary could not be located. "
    echo "Download and install the Java Synology package from http://wp.me/pVshC-z5"
    exit 1
  fi
  
  #is the User Home service enabled?
  UH_SERVICE=maybe
  synouser --add userhometest Testing123 "User Home test user" 0 "" ""
  UHT_HOMEDIR=`cat /etc/passwd | sed -r '/User Home test user/!d;s/^.*:User Home test user:(.*):.*$/\1/'`
  if echo $UHT_HOMEDIR | grep '/var/services/homes/' > /dev/null; then
    if [ ! -d $UHT_HOMEDIR ]; then
      UH_SERVICE=false
    fi
  fi
  synouser --del userhometest
  #remove home directory (needed since DSM 4.1)
  [ -e /var/services/homes/userhometest ] && rm -r /var/services/homes/userhometest
  if [ "${UH_SERVICE}" == "false" ]; then
    echo "The User Home service is not enabled. Please enable this feature in the User control panel in DSM."
    exit 1
  fi
  
  CP_BINARY_FOUND=
  [ -f ${PUBLIC_FOLDER}/${DOWNLOAD_FILE}.tgz ] && CP_BINARY_FOUND=true
  [ -f ${PUBLIC_FOLDER}/${DOWNLOAD_FILE}.tar ] && CP_BINARY_FOUND=true
  [ -f ${PUBLIC_FOLDER}/${DOWNLOAD_FILE}.tar.tar ] && CP_BINARY_FOUND=true
  [ -f ${PUBLIC_FOLDER}/${DOWNLOAD_FILE}.gz ] && CP_BINARY_FOUND=true
  
  if [ -z ${CP_BINARY_FOUND} ]; then
    echo "CrashPlan PROe server Linux installer not found. "
    echo "I was expecting the file ${PUBLIC_FOLDER}/${DOWNLOAD_FILE}.tgz "
    echo "Please visit crashplan.com, download the installer from ${DOWNLOAD_URL} "
    echo "and place it in the 'public' shared folder on your NAS."
    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 ()
{
  VAROPTDIR=${MANIFEST_FOLDER}/data
  VARLOGDIR=${MANIFEST_FOLDER}/log
  ETCDIR=${OPTDIR}/bin
  INITDIR=${OPTDIR}/bin
  RUNLVLDIR=${OPTDIR}/bin
  
  #create daemon user
  synouser --add ${DAEMON_USER} ${DAEMON_PASS} "${DAEMON_ID}" 0 "" ""
  
  #save the daemon user's homedir as variable in that user's profile
  #this is needed because new users seem to inherit a HOME value of /root which they have no permissions for.
  su - ${DAEMON_USER} -s /bin/sh -c "echo export HOME=\'${DAEMON_HOME}\' >> .profile"

  #extract CPU-specific additional binaries
  mkdir ${SYNOPKG_PKGDEST}/bin
  cd ${SYNOPKG_PKGDEST}/bin
  tar xzf ${TEMP_FOLDER}/${NATIVE_BINS_FILE} && rm ${TEMP_FOLDER}/${NATIVE_BINS_FILE}

  mkdir -p ${OPTDIR}
  mkdir -p ${INITDIR}
  mkdir -p ${RUNLVLDIR}
  mkdir -p ${VAROPTDIR}
  mkdir -p ${VARLOGDIR}
  cd ${PUBLIC_FOLDER}
  
  #extract CrashPlan Linux installer (Web browsers love to interfere with .tar.gz files)
  if [ -f ${DOWNLOAD_FILE}.tgz ]; then
    #Firefox seems to be the only browser that leaves it alone
    tar xzf ${DOWNLOAD_FILE}.tgz
  elif [ -f ${DOWNLOAD_FILE}.gz ]; then
    #Chrome
    tar xzf ${DOWNLOAD_FILE}.gz
  elif [ -f ${DOWNLOAD_FILE}.tar ]; then
    #Safari
    tar xf ${DOWNLOAD_FILE}.tar
  elif [ -f ${DOWNLOAD_FILE}.tar.tar ]; then
    #Internet Explorer
    tar xzf ${DOWNLOAD_FILE}.tar.tar
  fi
  
  mkdir -p ${OPTDIR}/content-custom
  mkdir -p ${OPTDIR}/installs
  mkdir ${VAROPTDIR}/backupArchives
  mkdir ${VAROPTDIR}/backupCache
  mkdir ${VAROPTDIR}/dumps
  chown -R ${DAEMON_USER} ${MANIFEST_FOLDER}
  
  #extract nested tgz archive
  cd ${OPTDIR}
  tar xozf "${PUBLIC_FOLDER}/${EXTRACTED_FOLDER}/${TGZ_FILE}"
  
  echo "#uncomment to expand Java max heap size beyond prescribed value of 1024M (will survive upgrades)" > ${SYNOPKG_PKGDEST}/syno_package.vars
  echo "#USR_MAX_HEAP=1024M" >> ${SYNOPKG_PKGDEST}/syno_package.vars
  echo >> ${SYNOPKG_PKGDEST}/syno_package.vars
  
  #create a valid identity file if there is no existing GUID
  GUID=
  if [ -f ${VAROPTDIR}/.identity ] ; then
    . ${VAROPTDIR}/.identity
  fi
  if [ "x${GUID}" == "x" ]; then
    echo -n "GUID=" > ${VAROPTDIR}/.identity
    java -cp "${OPTDIR}/lib/com.backup42.app.jar" com.code42.utils.UniqueId >> ${VAROPTDIR}/.identity
    . ${VAROPTDIR}/.identity
    if [ "x${GUID}" == "x" ] ; then
      echo "Failed to create valid server identity. Identity Path: ${VAROPTDIR}/.identity"
      exit 1
    fi
  fi
 
  #amend entries in default server config file
  sed -i "s%<OPT>%${OPTDIR}%" ${OPTDIR}/conf/conf_proe.properties
  sed -i "s%<VAROPT>%${VAROPTDIR}%" ${OPTDIR}/conf/conf_proe.properties
  sed -i "s%<VARLOGDIR>%${VARLOGDIR}%" ${OPTDIR}/conf/conf_proe.properties
  
  #save install variables which Crashplan expects its own installer script to create
  echo "" > ${VARS_FILE}
  echo "OPTDIR=${OPTDIR}" >> ${VARS_FILE}
  echo "VAROPTDIR=${VAROPTDIR}" >> ${VARS_FILE}
  echo "VARLOGDIR=${VARLOGDIR}" >> ${VARS_FILE}
  echo "ETCDIR=${ETCDIR}" >> ${VARS_FILE}
  echo "INITDIR=${INITDIR}" >> ${VARS_FILE}
  echo "RUNLVLD=${RUNLVLDIR}" >> ${VARS_FILE}
  echo "INSTALLDATE=`date +%Y%m%d`" >> ${VARS_FILE}
  echo "JAVACOMMON=\${JAVA_HOME}/bin/java" >> ${VARS_FILE}

  #remove temp files
  rm -r ${PUBLIC_FOLDER}/${EXTRACTED_FOLDER}
  
  #change owner of CrashPlan folder tree
  chown -R ${DAEMON_USER} ${SYNOPKG_PKGDEST}

  echo "CrashPlan PROe Server has been installed. When you start the package a few moments of first-time initialization "
  echo "are needed before the management application will be available in your web browser. You can check the Log tab "
  echo "to discover when it has fully started. "
  echo "http://localhost:4280/console "
  echo "https://localhost:4285/console "
  echo "Please note that your clients will communicate with the server on TCP port 4282."
  
  exit 0
}


preuninst ()
{
  #make sure engine is stopped
  su - ${DAEMON_USER} -s /bin/sh -c "${OPTDIR}/bin/${ENGINE_SCRIPT} stop"
  sleep 2
  
  exit 0
}


postuninst ()
{
  if [ -f ${SYNOPKG_PKGDEST}/syno_package.vars ]; then
    source ${SYNOPKG_PKGDEST}/syno_package.vars
  fi
  
  if [ "${LIBFFI_SYMLINK}" == "YES" ]; then
    rm /lib/libffi.so.5
  fi

  #if it doesn't exist, but is still a link then it's a broken link and should also be deleted
  if [ ! -e /lib/libffi.so.5 ]; then
    [ -L /lib/libffi.so.5 ] && rm /lib/libffi.so.5
  fi

  #remove daemon user
  synouser --del ${DAEMON_USER}
  
  #remove daemon user's home directory (needed since DSM 4.1)
  [ -e /var/services/homes/${DAEMON_USER} ] && rm -r /var/services/homes/${DAEMON_USER}
  
  exit 0
}

preupgrade ()
{
  #make sure engine is stopped
  su - ${DAEMON_USER} -s /bin/sh -c "${OPTDIR}/bin/${ENGINE_SCRIPT} stop"
  sleep 2
  
  #if config data exists back it up
  if [ -d ${OPTDIR}/keys ]; then
    mkdir -p ${SYNOPKG_PKGDEST}/../${DAEMON_USER}_data_mig
    for FOLDER_TO_MIGRATE in ${UPGRADE_FOLDERS}; do
      if [ -d ${OPTDIR}/${FOLDER_TO_MIGRATE} ]; then
        mv ${OPTDIR}/${FOLDER_TO_MIGRATE} ${SYNOPKG_PKGDEST}/../${DAEMON_USER}_data_mig
      fi
    done
  fi

  exit 0
}


postupgrade ()
{
  #use the migrated config data from the previous version
  if [ -d ${SYNOPKG_PKGDEST}/../${DAEMON_USER}_data_mig/keys ]; then
    for FOLDER_TO_MIGRATE in ${UPGRADE_FOLDERS}; do
    if [ -d ${SYNOPKG_PKGDEST}/../${DAEMON_USER}_data_mig/${FOLDER_TO_MIGRATE} ]; then
      cp -R ${SYNOPKG_PKGDEST}/../${DAEMON_USER}_data_mig/${FOLDER_TO_MIGRATE} ${OPTDIR}
    fi
    done
    rmdir ${SYNOPKG_PKGDEST}/../${DAEMON_USER}_data_mig

    #make log entry
    TIMESTAMP="`date +%x` `date +%I:%M%p`"
    echo "I ${TIMESTAMP} Synology Package Center updated ${SYNOPKG_PKGNAME} to version ${SYNOPKG_PKGVER}" >> ${LOG_FILE}
    
    #daemon user has been deleted and recreated so we need to reset ownership (new UID)
    chown -R ${DAEMON_USER} ${SYNOPKG_PKGDEST}
  fi
  
  exit 0
}
 

start-stop-status.sh

#!/bin/sh

#--------CRASHPLAN PROe server start-stop-status script
#--------package maintained at pcloadletter.co.uk

DAEMON_USER="`echo ${SYNOPKG_PKGNAME} | awk {'print tolower($_)'}`"
DAEMON_HOME="/var/services/homes/${DAEMON_USER}"
OPTDIR="${SYNOPKG_PKGDEST}/proserver/server"
TEMP_FOLDER="`find / -maxdepth 2 -name '@tmp' | head -n 1`"
MANIFEST_FOLDER="/`echo $TEMP_FOLDER | cut -f2 -d'/'`/${DAEMON_USER}"
VARLOGDIR=${MANIFEST_FOLDER}/log
LOG_FILE="${VARLOGDIR}/history.log.0"
ENGINE_SCRIPT="proserver"
APP_NAME="CPServer"
SCRIPTS_TO_EDIT="${ENGINE_SCRIPT} proservermonitor"
ENGINE_CFG="${ENGINE_SCRIPT}"
LIBFFI_SO_NAMES="5 6" #armada370 build of libjnidispatch.so is newer, and uses libffi.so.6
CFG_PARAM="JAVA_MEM_ARGS"
#note that the vars in the next two string values are escaped for evaluation later on
JAVA_MEM_ARGS="-Xss128k -Xms\${JAVA_MIN_HEAP}m -Xmx\${JAVA_MAX_HEAP}m"
JAVA_GC_ARGS="-XX:+DisableExplicitGC -XX:+UseAdaptiveGCBoundary -XX:PermSize=\${JAVA_MIN_HEAP}m -XX:MaxPermSize=\${JAVA_MIN_HEAP}m"
source ${OPTDIR}/.install.vars

JAVA_MIN_HEAP=`grep "^${CFG_PARAM}=" "${OPTDIR}/bin/${ENGINE_CFG}" | sed -r "s/^.*-Xms([0-9]+)[Mm] .*$/\1/"` 
SYNO_CPU_ARCH="`uname -m`"

case $1 in
  start)
    #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 
    SYNO_TZ=`cat /etc/synoinfo.conf | grep timezone | cut -f2 -d'"'`
    SYNO_TZ=`grep "^${SYNO_TZ}" /usr/share/zoneinfo/Timezone/tzname | sed -e "s/^.*= //"`
    grep "^export TZ" ${DAEMON_HOME}/.profile > /dev/null \
     && sed -i "s%^export TZ=.*$%export TZ='${SYNO_TZ}'%" ${DAEMON_HOME}/.profile \
     || echo export TZ=\'${SYNO_TZ}\' >> ${DAEMON_HOME}/.profile
    #check persistent variables from syno_package.vars
    USR_MAX_HEAP=0
    if [ -f ${SYNOPKG_PKGDEST}/syno_package.vars ]; then
      source ${SYNOPKG_PKGDEST}/syno_package.vars
    fi
    USR_MAX_HEAP=`echo $USR_MAX_HEAP | sed -e "s/[mM]//"`

    #create or repair libffi symlink if a DSM upgrade has removed it
    for FFI_VER in ${LIBFFI_SO_NAMES}; do 
      if [ -e ${OPTDIR}/lib/libffi.so.${FFI_VER} ]; then
        if [ ! -e /lib/libffi.so.${FFI_VER} ]; then
          #if it doesn't exist, but is still a link then it's a broken link and should be deleted
          [ -L /lib/libffi.so.${FFI_VER} ] && rm /lib/libffi.so.${FFI_VER}
          ln -s ${OPTDIR}/lib/libffi.so.${FFI_VER} /lib/libffi.so.${FFI_VER}
        fi
      fi
    done

    #fix up some of the binary paths and fix some command syntax for busybox 
    #moved this to start-stop-status from installer.sh because Code42 push updates and these
    #new scripts will need this treatment too
    FIND_TARGETS=
    for TARGET in ${SCRIPTS_TO_EDIT}; do
      FIND_TARGETS="${FIND_TARGETS} -o -name ${TARGET}"
    done
    find ${OPTDIR} \( -name \*.sh ${FIND_TARGETS} \) | while IFS="" read -r FILE_TO_EDIT; do
      if [ -e ${FILE_TO_EDIT} ]; then
        #this list of substitutions will probably need expanding as new CrashPlan updates are released
        sed -i "s%^#!/bin/bash%#!${SYNOPKG_PKGDEST}/bin/bash%" "${FILE_TO_EDIT}"
        sed -i -r "s%(^\s*)nice -n%\1${SYNOPKG_PKGDEST}/bin/nice -n%" "${FILE_TO_EDIT}"
        sed -i -r "s%(^\s*)(/bin/ps|ps) [^\|]*\|%\1/bin/ps w \|%" "${FILE_TO_EDIT}"
        sed -i -r "s%\`ps [^\|]*\|%\`ps w \|%" "${FILE_TO_EDIT}"
        sed -i "s/rm -fv/rm -f/" "${FILE_TO_EDIT}"
        sed -i "s/mv -fv/mv -f/" "${FILE_TO_EDIT}"
      fi
    done

    #an upgrade script that has been launched via the web app will usually have failed until the above
    #changes are made so we need to find it and start it, if it exists
    UPGRADE_SCRIPT=`find ${OPTDIR}/upgrade -name "upgrade.sh"`
    if [ -n "${UPGRADE_SCRIPT}" ]; then
      rm ${OPTDIR}/${ENGINE_SCRIPT}.pid
      SCRIPT_HOME=`dirname $UPGRADE_SCRIPT`

      #make CrashPlan log entry
      TIMESTAMP="`date +%x` `date +%I:%M%p`"
      echo "I ${TIMESTAMP} Synology repairing upgrade in ${SCRIPT_HOME}" >> ${LOG_FILE}

      mv ${SCRIPT_HOME}/upgrade.log ${SCRIPT_HOME}/upgrade.log.old
      chown -R ${DAEMON_USER} ${SYNOPKG_PKGDEST}
      su - ${DAEMON_USER} -s /bin/sh -c "cd ${SCRIPT_HOME} ; . upgrade.sh"
      mv ${SCRIPT_HOME}/upgrade.sh ${SCRIPT_HOME}/upgrade.sh.old
      exit 0
    fi

    #updates may also overwrite our native binaries
    if [ "${SYNO_CPU_ARCH}" != "x86_64" ]; then
      cp -f ${SYNOPKG_PKGDEST}/bin/libjtux.so ${OPTDIR}
      cp -f ${SYNOPKG_PKGDEST}/bin/jna-3.2.5.jar ${OPTDIR}/lib
      cp -f ${SYNOPKG_PKGDEST}/bin/libffi.so.* ${OPTDIR}/lib
    fi

    #set appropriate Java max heap size
    RAM=$((`free | grep Mem: | sed -e "s/^ *Mem: *\([0-9]*\).*$/\1/"`/1024))
    if [ $RAM -le 128 ]; then
      JAVA_MAX_HEAP=80
    elif [ $RAM -le 256 ]; then
      JAVA_MAX_HEAP=192
    elif [ $RAM -le 512 ]; then
      JAVA_MAX_HEAP=384
    elif [ $RAM -le 1024 ]; then
      JAVA_MAX_HEAP=896
    #CrashPlan PROe server's default max heap is 1GB
    elif [ $RAM -gt 1024 ]; then
      JAVA_MAX_HEAP=1024
    fi
    if [ $USR_MAX_HEAP -gt $JAVA_MAX_HEAP ]; then
      JAVA_MAX_HEAP=${USR_MAX_HEAP}
    fi
    if [ $JAVA_MAX_HEAP -lt $JAVA_MIN_HEAP ]; then
      #can't have a max heap lower than min heap (ARM low RAM systems)
      JAVA_MIN_HEAP=${JAVA_MAX_HEAP}
    fi

    #reset ownership of all files to daemon user, so that manual edits to config files won't cause problems
    chown -R ${DAEMON_USER} ${SYNOPKG_PKGDEST}
    chown -R ${DAEMON_USER} ${DAEMON_HOME}    
    
    #CrashPlan PROe server will read customized vars from a separate file
    eval echo "JAVA_GC_ARGS='\"'${JAVA_GC_ARGS}'\"'" > ${OPTDIR}/.proserverrc
    eval echo "JAVA_MEM_ARGS='\"'${JAVA_MEM_ARGS}'\"'" >> ${OPTDIR}/.proserverrc
    su - ${DAEMON_USER} -s /bin/sh -c "${OPTDIR}/bin/${ENGINE_SCRIPT} start"
    exit 0
  ;;

  stop)
    su - ${DAEMON_USER} -s /bin/sh -c "${OPTDIR}/bin/${ENGINE_SCRIPT} stop"
    exit 0
  ;;

  status)
    PID=`/bin/ps w| grep "app=${APP_NAME}" | grep -v grep | awk '{ print $1 }'`
    if [[ -n "$PID" ]]; then
      exit 0
    else
      exit 1
    fi
  ;;

  log)
    echo "${LOG_FILE}"
    exit 0
  ;;
esac
 

Changelog:

  • 020 added support for Intel Atom Evansport CPU in some new DSx14 products
  • 019 update to CrashPlan PROe Server 3.5.5, improved update handling
  • 018 update to CrashPlan PROe Server 3.4.1.5, improved update handling, fixes for DSM 4.2
  • 017 update to CrashPlan PROe Server 3.4.1, improved update handling
  • 016 update to CrashPlan PROe Server 3.3.0.4
  • 015 further fixes for the update mechanism
  • 014 created a wrapper for the ps command, and a symlink for /bin/bash which should hopefully allow server upgrade scripts from Code 42 to run
  • 013 fixed a timezone detection bug
  • 012 fixed a bug with the script editing logic which amends Code 42′s scripts to work with busybox shell tools
  • 011 updated to CrashPlan PROe Server 3.3.0.3
  • 010 intial public release
 
 

Corrupt Windows 7 NTFS junction points

I encountered an unusual problem recently – all Windows 7 workstations which had been built with a Microsoft Select Agreement Volume License version of Windows 7 Professional RTM using an unattended install, not via sysprep, had some sort of damage to their legacy filesystem junction points. This had prevented the installer for Kaspersky EndPoint Protection 8 and its Network Agent version 9 from running, though earlier versions had been fine. The error took Kaspersky support a very long time to pin down (several months in fact, despite them having detailed MSI installer logs), and it eventually transpired that many of the links to maintain legacy OS compatibility like C:\Documents and Settings -> C:\Users, or C:\Users\All Users -> C:\ProgramData on these affected systems were resolving to some kind of temporary mounted WIM image path, within the folder C:\Users\ADMINI~1\AppData\Local\Temp\mnt\wim.

This folder no longer existed, and nor was there any phantom mounted WIM image, so any attempt to access the damaged links would fail (in Kaspersky’s case the issue was C:\ProgramData\Application Data). I still have no idea what may have caused this. More recently the unattended install I designed uses Windows 7 Enterprise SP1, with no changes to the core build scripting, and systems built from this do not exhibit this issue. This might suggest it was a problem with Windows itself, and if so then my script to fix the damage could be useful for others.

The repair script requires SetACL.exe which is an extremely versatile tool, but which is syntactically very difficult to use! I compared the ACLs on a clean system, noted the link type (they’re not all junctions, there is one symlink), and whether or not there were deny permissions which prevent recursion on links which resolve to their parent folder e.g. C:\ProgramData\Application Data -> C:\ProgramData. The links are deleted and recreated, but only on systems that are detected to need the fix (see the highlighted line for that logic). If you set line 6 to “set DEBUG=echo” you can test the output before actually invoking the repair commands.

@echo off

:: Windows 7 junction point/symlink fix script
:: patters 13/03/2012

set DEBUG=
setlocal

dir /aL C:\ProgramData | find /I "C:\Users\ADMINI~1\AppData\Local\Temp\mnt\wim\" && (
  call :junction /J "C:\Documents and Settings" "C:\Users" deny
  
  call :junction /J "C:\ProgramData\Application Data" "C:\ProgramData" deny
  call :junction /J "C:\ProgramData\Desktop" "C:\Users\Public\Desktop" deny
  call :junction /J "C:\ProgramData\Documents" "C:\Users\Public\Documents" deny
  call :junction /J "C:\ProgramData\Favorites" "C:\Users\Public\Favorites" deny
  call :junction /J "C:\ProgramData\Start Menu" "C:\ProgramData\Microsoft\Windows\Start Menu" nodeny
  call :junction /J "C:\ProgramData\Templates" "C:\ProgramData\Microsoft\Windows\Templates" deny
  
  call :junction /D "C:\Users\All Users" "C:\ProgramData" deny
  call :junction /J "C:\Users\All Users\Application Data" "C:\ProgramData" deny
  call :junction /J "C:\Users\All Users\Desktop" "C:\Users\Public\Desktop" deny
  call :junction /J "C:\Users\All Users\Documents" "C:\Users\Public\Documents" deny
  call :junction /J "C:\Users\All Users\Favorites" "C:\Users\Public\Favorites" deny
  call :junction /J "C:\Users\All Users\Start Menu" "C:\ProgramData\Microsoft\Windows\Start Menu" nodeny
  call :junction /J "C:\Users\All Users\Templates" "C:\ProgramData\Microsoft\Windows\Templates" deny
  call :junction /J "C:\Users\Public\Documents\My Music" "C:\Users\Public\Music" deny
  call :junction /J "C:\Users\Public\Documents\My Pictures" "C:\Users\Public\Pictures" deny
  call :junction /J "C:\Users\Public\Documents\My Videos" "C:\Users\Public\Videos" deny
  
  call :junction /J "C:\Users\Default User" "C:\Users\Default" deny
  call :junction /J "C:\Users\Default\Application Data" "C:\Users\Default\AppData\Roaming" deny
  call :junction /J "C:\Users\Default\Cookies" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Cookies" deny
  call :junction /J "C:\Users\Default\Local Settings" "C:\Users\Default\AppData\Local" deny
  call :junction /J "C:\Users\Default\My Documents" "C:\Users\Default\Documents" deny
  call :junction /J "C:\Users\Default\NetHood" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Network Shortcuts" deny
  call :junction /J "C:\Users\Default\PrintHood" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Printer Shortcuts" deny
  call :junction /J "C:\Users\Default\Recent" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Recent" deny
  call :junction /J "C:\Users\Default\SendTo" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\SendTo" deny
  call :junction /J "C:\Users\Default\Start Menu" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu" deny
  call :junction /J "C:\Users\Default\Templates" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Templates" deny
  call :junction /J "C:\Users\Default\Documents\My Music" "C:\Users\Default\Music" deny
  call :junction /J "C:\Users\Default\Documents\My Pictures" "C:\Users\Default\Pictures" deny
  call :junction /J "C:\Users\Default\Documents\My Videos" "C:\Users\Default\Videos" deny
  call :junction /J "C:\Users\Default\AppData\Local\Application Data" "C:\Users\Default\AppData\Local" deny
    
  call :junction /J "C:\Users\Default\AppData\Local\Temporary Internet Files" "C:\Users\Default\AppData\Local\Microsoft\Windows\Temporary Internet Files" deny
) || echo Legacy filesystem junction points/symlinks are fine.

::odd permissions for this one, so I'm leaving it out
::call :junction /J "C:\Users\Default\AppData\Local\History" "C:\Users\Default\AppData\Local\Microsoft\Windows\History" deny

goto :eof

:junction
:: %1 = type (junction or directory symlink)
:: %2 = junction/symlink path
:: %3 = target path
:: %4 = set the deny permission or not

::delete old junction point
%DEBUG% rmdir "%~2"

::create new junction point
%DEBUG% mklink %1 "%~2" "%~3"

::set owner to SYSTEM
%DEBUG% setacl -on "%~2" -ot file -actn setowner -ownr "n:SYSTEM"

:: we need to stop inheritance of permissions before we make changes. This must be done with
:: a separate commandline entry owing to the order in which SetACL.exe processes its arguments.
%DEBUG% setacl -on "%~2" -ot file -actn setprot -op "dacl:p_c;sacl:p_c"

::clear ACL and set permissions
%DEBUG% setacl -on "%~2" -ot file -actn clear -clr "dacl,sacl" -actn ace -ace "n:Everyone;i:np;p:read_ex" -actn ace -ace "n:SYSTEM;i:np;p:full" -actn ace -ace "n:Administrators;i:np;p:full"

::add directory listing deny permission for recursive paths if needed
if "%4"=="deny" %DEBUG% setacl -on "%~2" -ot file -actn ace -ace "n:Everyone;s:n;m:deny;i:np;p:list_dir"