#!/usr/bin/env ruby
# frozen_string_literal: true

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.fetch('ALLUSERSPROFILE', nil), '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
      while service.running?
        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
    @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)
    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
    unless eventlog.nil?
      eventlog.close
    end
  end

  # Parses runinterval.
  #
  # @param puppet_path [String] The file path for the Puppet executable.
  # @return runinterval [Integer] How often to do a Puppet run, in seconds.
  def parse_runinterval(puppet_path)
    begin
      runinterval = %x(#{puppet_path} config --section agent --log_level notice print runinterval).chomp
      if runinterval == ''
        runinterval = 1800
        log_err("Failed to determine runinterval, defaulting to #{runinterval} seconds")
      else
        # Use Kernel#Integer because to_i will return 0 with non-numeric strings.
        runinterval = Integer(runinterval)
      end
    rescue Exception => e
      log_exception(e)
      runinterval = 1800
    end

    runinterval
  end

  def parse_log_level(puppet_path, cmdline_debug)
    begin
      loglevel = "notice"
      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 || loglevel.to_sym)
  end

  private

  def load_env(base_dir)
    # 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.fetch('Path', nil)

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

if __FILE__ == $PROGRAM_NAME
  WindowsDaemon.mainloop
end
