Wednesday, July 15, 2015

Windows Scripting with Ruby


Windows world is full of clicks. For some people, this is a nightmare.

While others may try to use batch file to automate, batch itself is too awkward and ancient with very limited capabilities.

There is Windows Powershell to allow more scripting, but for some people, powershell is still "not good enough" (or read: syntaces are too dirty and messy) compared to other popular script languages like Python, Ruby or Groovy.

Below is an example install script written with Ruby to perform simple file copies and eliminate the needs of writing Powershell:
  1. Delete or backup files if previous installation is detected
  2. Copy files from source to destination
  3. Perform Windows registry setup
  4. Perform auto-reboot if certain files cannot be copied due to file locking by other Windows process

install_program.rb
require 'fileutils'

$REBOOT_ON_ERROR = true #Change to false if we don't want to auto-reboot upon exception error (e.g. during debugging)
$INSTALL_RETRY_BAT = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\installretry.bat' #Startup location on Windows 7
$IS_RESUMING = File.exist?( $INSTALL_RETRY_BAT )

def copy_files
    _TARGET_FOLDER = 'C:\MyApp\RubyApp'
    FileUtils.mv(_TARGET_FOLDER, "#{_TARGET_FOLDER}_#{Time.now.strftime "%Y-%m-%d_%H-%M-%S"}") if (Dir.exist? _TARGET_FOLDER) #Make backup instead of overwriting it
    FileUtils::mkdir_p(_TARGET_FOLDER)
    FileUtils.cp_r(".\\RubyAppSource\\", "#{_TARGET_FOLDER}")
end

def configure_windows
    system( "net stop \"Windows Time\" /y 2>&1" )
    2.times do #Handle Windows bug (https://whiskykilo.com/w32tm-or-w32time-or-whatever-ms-wants-to-call-it/)
        system( "w32tm /unregister >nul 2>&1" ) 
        system( "w32tm /unregister >nul 2>&1" ) 
        system( "w32tm /register >nul 2>&1" )
    end
    system( "reg add \"HKLM\\SYSTEM\\CurrentControlSet\\services\\W32Time\\TimeProviders\\NtpServer\" /v Enabled /t REG_DWORD /d 1 /f 2>&1" )
    system( "reg add \"HKLM\\SYSTEM\\CurrentControlSet\\services\\W32Time\\Config\" /v AnnounceFlags /t REG_DWORD /d 5 /f 2>&1" )
    system( "net start \"Windows Time\" /y 2>&1" )
    system( "w32tm /config /update 2>&1" )
end

def show_retry_if_it_is_executed_from_installretry_bat
    if $IS_RESUMING
        puts "*** Retrying installation after previous error and reboot ***" 
        sleep 10
    end
end

def do_auto_reboot_and_retry
    if $REBOOT_ON_ERROR == true and !$IS_RESUMING
        puts "Script engine is trying to perform reboot on error now..."
        File.open( $INSTALL_RETRY_BAT, "w" ) { |file| file.write( File.expand_path($0).gsub('/', '\\') ) }
        system "shutdown /f /r /t 1 2>&1" 
    else
        ( puts "Fatal error, still unable to complete installation after the reboot" ) if $IS_RESUMING
    end
end

if (__FILE__ == $0)
    begin
        show_retry_if_it_is_executed_from_installretry_bat()
        copy_files()
        configure_windows()
        puts "Installation done"
    rescue => e
        do_auto_reboot_and_retry() #in case some files or folders are locked and require reboot during copy_files()
    ensure
        ( FileUtils.rm( $INSTALL_RETRY_BAT ) rescue nil ) if $IS_RESUMING #Ensure we resume only ONCE, not repeatedly
        puts "Press ENTER to exit"
        $stdin.gets
    end
end

That's it.
We don't need fancy tool like WiX or InstallShield. Just fire the text editor and then run it.

And if we want to call WinAPI, we can use FFI or Win32API for it

This is an example on how to fire Visual Studio COM automation using WIN32OLE:
require 'win32ole'

_MY_SOLUTION_SLN='D:/myvisualstudio_projects/project1/solution1.sln'
_dte = WIN32OLE.new('VisualStudio.DTE.12.0') rescue WIN32OLE.new('VisualStudio.DTE.10.0') rescue raise "Cannot find Visual Studio 2010 nor 2013"
_vs_version = _dte.RegistryRoot[-4..-1] rescue ""
puts "\r\nDetected Visual Studio version: #{_vs_version}"

_dte.SuppressUI = false
_mainWindow = _dte.MainWindow
_mainWindow.Visible = true
_mainWindow.WindowState = 2 #maximize
#_mainWindow.WindowState = 0 #restore
#_mainWindow.WindowState = 1 #minimize

_sln = _dte.Solution
_sln.Open("#{_MY_SOLUTION_SLN}") 

Happy scripting with Ruby :)