Create a Windows Service with Ruby - Part 2

August 2, 2013

In the last post I created a simple Windows Service using Ruby. The biggest drawback of creating a Windows Service with Ruby is that the target machine would need Ruby installed as a prerequisite. Fortunately there is a Ruby gem called OCRA which alleviates this issue.

According to OCRA’s GitHub page,

OCRA (One-Click Ruby Application) builds Windows executables from Ruby source code. The executable is a self-extracting, self-running executable that contains the Ruby interpreter, your source code and any additionally needed ruby libraries or DLL.

There are a few changes that will need to be made in order to get the Windows Service working with OCRA.

register.rb

require 'rubygems'
require 'win32/service'
include Win32

unless defined?(Ocra)
  Service.create({
    service_name: 'testservice',
    host: nil,
    service_type: Service::WIN32_OWN_PROCESS,
    description: 'Test Service',
    start_type: Service::AUTO_START,
    error_control: Service::ERROR_NORMAL,
    binary_path_name: "#{`echo %cd%`.chomp}\\service.exe",
    load_order_group: 'Network',
    dependencies: nil,
    display_name: 'Test Service'
  })

  Service.start("testservice")
end

Two changes were made to register.rb. The first change was to wrap the Service.create and Service.start calls in an ‘unless’ block which checks if OCRA is defined in the current environment. This is necessary because OCRA executes the script it is packaging in order to determine dependencies, and we don’t want the bundling process to create and start the Windows Service.

The second change was to point the Windows Service at the service executable we will be creating. We no longer need to have Ruby execute the script because the Ruby runtime will be packaged with the executable.

service.rb

require 'rubygems'
require 'win32/daemon'
include Win32

class TestDaemon < Daemon
  def service_main
      log 'started'
      while running?
        log 'running'
        sleep 10
      end
  end
  
  def service_stop
    log 'ended'
    exit!
  end
  
  def log(text)
    File.open(File.expand_path("../log.txt", ENV["OCRA_EXECUTABLE"] || __FILE__) , 'a') { |f| f.puts "#{Time.now}: #{text}" }
  end
end

TestDaemon.mainloop unless defined?(Ocra)

Two changes were also made to service.rb. The first change was to only run the daemon if the script was not being packaged by OCRA. This may be unnecessary because running the mainloop function manually doesn’t seem to do anything if the script isn’t being run as a Windows Service.

The second change was the result of a lot of frustration and head scratching. After packaging the service with OCRA none of the conventional ways of getting the current directory were returning the executable’s parent directory. I believe the cause of this issue is that when a Windows Service is run the current working directory is set to C:\Windows\System32 (or the equivalent on 64 bit systems).

OCRA does have a command line option –ch-dir, that is supposed to change the working directory to the current directory at run time, but this doesn’t seem to work in the context of a Windows Service.

Fortunately OCRA provides another solution. According to the OCRA GitHub readme,

bq. When the application is running, the OCRA_EXECUTABLE environment variable points to the .exe (with full path).

So to solve this issue I simply use the OCRA_EXECUTABLE environment variable if its available, and if not I fall back to the current source directory. The fallback isn’t strictly necessary unless you wish to run the script directly using Ruby, as opposed to running the executable generated by OCRA.

require 'rubygems'
require 'win32/service'
include Win32

unless defined?(Ocra)
  SERVICE = 'testservice'
  begin
    Service.stop(SERVICE) if Service.status(SERVICE).controls_accepted.include? "stop" 
  rescue
  end
  Service.delete(SERVICE)
end

The only change to unregister.rb was to wrap it in the OCRA ‘unless’ block to prevent execution on packaging.

Now that the script changes are made the final step is to install OCRA and package the scripts.

gem install ocra

ocra register.rb
ocra service.rb
ocra unregister.rb

These commands will generate the executables register.exe, service.exe, and unregister.exe. Now if you run register.exe, with admin privileges, it will create a Windows Service that runs the service.exe application in the same directory.

I created a GitHub project with the example code as well as the generated executables.

After working out the kinks I was able to create a Windows Service for a production application using Ruby. The simplicity of Ruby paired with the amazing wealth of third party gems makes developing Windows Services with Ruby a very attractive option.

Steve Johnson

Steve Johnson

Full Stack Engineer
Product Developer
GitHub LinkedIn Twitter