#!/usr/bin/env ruby

require 'fileutils'
require 'puppet/util/windows/daemon'

# This file defines utilities for logging to eventlog. While it lives inside
# Puppet, it is completely independent and loads no other parts of Puppet, so we
# can safely require *just* it.
require 'puppet/util/windows/eventlog'

# monkey patches ruby Process to add .create method
require 'puppet/util/windows/monkey_patches/process'

class WindowsDaemon < Puppet::Util::Windows::Daemon
  CREATE_NEW_CONSOLE          = 0x00000010

  @run_thread = nil
  @LOG_TO_FILE = false
  @loglevel = 0
  LOG_FILE =  File.expand_path(File.join(ENV['ALLUSERSPROFILE'], 'PuppetLabs', 'puppet', 'var', 'log', 'windows.log'))
  LEVELS = [:debug, :info, :notice, :warning, :err, :alert, :emerg, :crit]
  LEVELS.each do |level|
    define_method("log_#{level}") do |msg|
      log(msg, level)
    end
  end

  def service_init
  end

  def service_main(*argsv)
    argsv = (argsv << ARGV).flatten.compact
    args = argsv.join(' ')
    @loglevel = LEVELS.index(argsv.index('--debug') ? :debug : :notice)

    @LOG_TO_FILE = (argsv.index('--logtofile') ? true : false)

    if (@LOG_TO_FILE)
      FileUtils.mkdir_p(File.dirname(LOG_FILE))
      args = args.gsub("--logtofile","")
    end

    base_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
    load_env(base_dir)

    # The puppet installer registers a 'Puppet' event source.  For the moment events will be logged with this key, but
    # it may be a good idea to split the Service and Puppet events later so it's easier to read in the windows Event Log.
    #
    # Example code to register an event source;
    # eventlogdll =  File.expand_path(File.join(basedir, 'puppet', 'ext', 'windows', 'eventlog', 'puppetres.dll'))
    # if (File.exist?(eventlogdll))
    #   Win32::EventLog.add_event_source(
    #      'source' => "Application",
    #      'key_name' => "Puppet Agent",
    #      'category_count' => 3,
    #      'event_message_file' => eventlogdll,
    #      'category_message_file' => eventlogdll
    #   )
    # end

    puppet = File.join(base_dir, 'puppet', 'bin', 'puppet')
    ruby = File.join(base_dir, 'puppet', 'bin', 'ruby.exe')
    ruby_puppet_cmd = "\"#{ruby}\" \"#{puppet}\""

    unless File.exist?(puppet)
      log_err("File not found: '#{puppet}'")
      return
    end
    log_debug("Using '#{puppet}'")

    cmdline_debug = argsv.index('--debug') ? :debug : nil
    @loglevel = parse_log_level(ruby_puppet_cmd, cmdline_debug)
    log_notice('Service started')

    service = self
    @run_thread = Thread.new do
      begin
        while service.running? do
          runinterval = service.parse_runinterval(ruby_puppet_cmd)

          if service.state == RUNNING or service.state == IDLE
            service.log_notice("Executing agent with arguments: #{args}")
            pid = Process.create(:command_line => "#{ruby_puppet_cmd} agent --onetime #{args}", :creation_flags => CREATE_NEW_CONSOLE).process_id
            service.log_debug("Process created: #{pid}")
          else
            service.log_debug("Service is paused.  Not invoking Puppet agent")
          end

          service.log_debug("Service worker thread waiting for #{runinterval} seconds")
          sleep(runinterval)
          service.log_debug('Service worker thread woken up')
        end
      rescue Exception => e
        service.log_exception(e)
      end
    end
    @run_thread.join

  rescue Exception => e
    log_exception(e)
  ensure
    log_notice('Service stopped')
  end

  def service_stop
    log_notice('Service stopping / killing worker thread')
    @run_thread.kill if @run_thread
  end

  def service_pause
    log_notice('Service pausing')
  end

  def service_resume
    log_notice('Service resuming')
  end

  def service_shutdown
    log_notice('Host shutting down')
  end

  # Interrogation handler is just for debug.  Can be commented out or removed entirely.
  # def service_interrogate
  #   log_debug('Service is being interrogated')
  # end

  def log_exception(e)
    log_err(e.message)
    log_err(e.backtrace.join("\n"))
  end

  def log(msg, level)
    if LEVELS.index(level) >= @loglevel
      if (@LOG_TO_FILE)
        # without this change its possible that we get Encoding errors trying to write UTF-8 messages in current codepage
        File.open(LOG_FILE, 'a:UTF-8') { |f| f.puts("#{Time.now} Puppet (#{level}): #{msg}") }
      end

      native_type, native_id = Puppet::Util::Windows::EventLog.to_native(level)
      report_windows_event(native_type, native_id, msg.to_s)
    end
  end

  def report_windows_event(type,id,message)
    begin
      eventlog = nil
      eventlog = Puppet::Util::Windows::EventLog.open("Puppet")
      eventlog.report_event(
        :event_type  => type,   # EVENTLOG_ERROR_TYPE, etc
        :event_id    => id,     # 0x01 or 0x02, 0x03 etc.
        :data        => message # "the message"
      )
    rescue Exception
      # Ignore all errors
    ensure
      if (!eventlog.nil?)
        eventlog.close
      end
    end
  end

  def parse_runinterval(puppet_path)
    begin
      runinterval = %x{ #{puppet_path} config --section agent --log_level notice print runinterval }.to_i
      if runinterval == 0
        runinterval = 1800
        log_err("Failed to determine runinterval, defaulting to #{runinterval} seconds")
      end
    rescue Exception => e
      log_exception(e)
      runinterval = 1800
    end

    runinterval
  end

  def parse_log_level(puppet_path,cmdline_debug)
    begin
      loglevel = %x{ #{puppet_path} config --section agent --log_level notice print log_level }.chomp
      unless loglevel && respond_to?("log_#{loglevel}")
        loglevel = :notice
        log_err("Failed to determine loglevel, defaulting to #{loglevel}")
      end
    rescue Exception => e
      log_exception(e)
      loglevel = :notice
    end

    LEVELS.index(cmdline_debug ? cmdline_debug : loglevel.to_sym)
  end

  private

  def load_env(base_dir)
    begin
      # ENV that uses backward slashes
      ENV['FACTER_env_windows_installdir'] = base_dir.tr('/', '\\')
      ENV['PL_BASEDIR'] = base_dir.tr('/', '\\')
      ENV['PUPPET_DIR'] = File.join(base_dir, 'puppet').tr('/', '\\')
      ENV['OPENSSL_CONF'] = File.join(base_dir, 'puppet', 'ssl', 'openssl.cnf').tr('/', '\\')
      ENV['SSL_CERT_DIR'] = File.join(base_dir, 'puppet', 'ssl', 'certs').tr('/', '\\')
      ENV['SSL_CERT_FILE'] = File.join(base_dir, 'puppet', 'ssl', 'cert.pem').tr('/', '\\')
      ENV['Path'] = [
        File.join(base_dir, 'puppet', 'bin'),
        File.join(base_dir, 'bin'),
      ].join(';').tr('/', '\\') + ';' + ENV['Path']

      # ENV that uses forward slashes
      ENV['RUBYLIB'] = "#{File.join(base_dir, 'puppet','lib')};#{ENV['RUBYLIB']}"
    rescue => e
      log_exception(e)
    end
  end
end

if __FILE__ == $0
  WindowsDaemon.mainloop
end
