File Manager
Viewing File: windows_service.rb
#
# Author:: Christopher Maier (<maier@lambda.local>)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require_relative "../../chef"
require_relative "../monologger"
require_relative "../application"
require_relative "../client"
require_relative "../config"
require_relative "../handler/error_report"
require_relative "../log"
require_relative "../http"
require "mixlib/cli" unless defined?(Mixlib::CLI)
require "socket" unless defined?(Socket)
require "uri" unless defined?(URI)
require "win32/daemon"
require_relative "../mixin/shell_out"
require_relative "../dist"
class Chef
class Application
class WindowsService < ::Win32::Daemon
include Mixlib::CLI
include Chef::Mixin::ShellOut
option :config_file,
short: "-c CONFIG",
long: "--config CONFIG",
default: "#{Chef::Config.etc_chef_dir}/client.rb",
description: "The configuration file to use for #{Chef::Dist::PRODUCT} runs."
option :log_location,
short: "-L LOGLOCATION",
long: "--logfile LOGLOCATION",
description: "Set the log file location."
option :splay,
short: "-s SECONDS",
long: "--splay SECONDS",
description: "The splay time for running at intervals, in seconds.",
proc: lambda { |s| s.to_i }
option :interval,
short: "-i SECONDS",
long: "--interval SECONDS",
description: "Set the number of seconds to wait between #{Chef::Dist::PRODUCT} runs.",
proc: lambda { |s| s.to_i }
DEFAULT_LOG_LOCATION ||= "#{Chef::Config.c_chef_dir}/client.log".freeze
def service_init
@service_action_mutex = Mutex.new
@service_signal = ConditionVariable.new
reconfigure
Chef::Log.info("#{Chef::Dist::CLIENT} Service initialized")
end
def service_main(*startup_parameters)
# Chef::Config is initialized during service_init
# Set the initial timeout to splay sleep time
timeout = rand Chef::Config[:splay]
while running?
# Grab the service_action_mutex to make a chef-client run
@service_action_mutex.synchronize do
begin
Chef::Log.info("Next #{Chef::Dist::CLIENT} run will happen in #{timeout} seconds")
@service_signal.wait(@service_action_mutex, timeout)
# Continue only if service is RUNNING
next if state != RUNNING
# Reconfigure each time through to pick up any changes in the client file
Chef::Log.info("Reconfiguring with startup parameters")
reconfigure(startup_parameters)
timeout = Chef::Config[:interval]
# Honor splay sleep config
timeout += rand Chef::Config[:splay]
# run chef-client only if service is in RUNNING state
next if state != RUNNING
Chef::Log.info("#{Chef::Dist::CLIENT} service is starting a #{Chef::Dist::CLIENT} run...")
run_chef_client
rescue SystemExit => e
# Do not raise any of the errors here in order to
# prevent service crash
Chef::Log.error("#{e.class}: #{e}")
rescue Exception => e
Chef::Log.error("#{e.class}: #{e}")
end
end
end
# Daemon class needs to have all the signal callbacks return
# before service_main returns.
Chef::Log.trace("Giving signal callbacks some time to exit...")
sleep 1
Chef::Log.trace("Exiting service...")
end
################################################################################
# Control Signal Callback Methods
################################################################################
def service_stop
run_warning_displayed = false
Chef::Log.info("STOP request from operating system.")
loop do
# See if a run is in flight
if @service_action_mutex.try_lock
# Run is not in flight. Wake up service_main to exit.
@service_signal.signal
@service_action_mutex.unlock
break
else
unless run_warning_displayed
Chef::Log.info("Currently a #{Chef::Dist::PRODUCT} run is happening on this system.")
Chef::Log.info("Service will stop when run is completed.")
run_warning_displayed = true
end
Chef::Log.trace("Waiting for #{Chef::Dist::PRODUCT} run...")
sleep 1
end
end
Chef::Log.info("Service is stopping....")
end
def service_pause
Chef::Log.info("PAUSE request from operating system.")
# We don't need to wake up the service_main if it's waiting
# since this is a PAUSE signal.
if @service_action_mutex.locked?
Chef::Log.info("Currently a #{Chef::Dist::PRODUCT} run is happening.")
Chef::Log.info("Service will pause once it's completed.")
else
Chef::Log.info("Service is pausing....")
end
end
def service_resume
# We don't need to wake up the service_main if it's waiting
# since this is a RESUME signal.
Chef::Log.info("RESUME signal received from the OS.")
Chef::Log.info("Service is resuming....")
end
def service_shutdown
Chef::Log.info("SHUTDOWN signal received from the OS.")
# Treat shutdown similar to stop.
service_stop
end
################################################################################
# Internal Methods
################################################################################
private
# Initializes Chef::Client instance and runs it
def run_chef_client
# The chef client will be started in a new process. We have used shell_out to start the chef-client.
# The log_location and config_file of the parent process is passed to the new chef-client process.
# We need to add the --no-fork, as by default it is set to fork=true.
Chef::Log.info "Starting #{Chef::Dist::CLIENT} in a new process"
# Pass config params to the new process
config_params = " --no-fork"
config_params += " -c #{Chef::Config[:config_file]}" unless Chef::Config[:config_file].nil?
# log_location might be an event logger and if so we cannot pass as a command argument
# but shed no tears! If the logger is an event logger, it must have been configured
# as such in the config file and chef-client will use that when no arg is passed here
config_params += " -L #{resolve_log_location}" if resolve_log_location.is_a?(String)
# Starts a new process and waits till the process exits
result = shell_out(
"#{Chef::Dist::CLIENT}.bat #{config_params}",
timeout: Chef::Config[:windows_service][:watchdog_timeout],
logger: Chef::Log
)
Chef::Log.trace (result.stdout).to_s
Chef::Log.trace (result.stderr).to_s
rescue Mixlib::ShellOut::CommandTimeout => e
Chef::Log.error "#{Chef::Dist::CLIENT} timed out\n(#{e})"
Chef::Log.error(<<-EOF)
Your #{Chef::Dist::CLIENT} run timed out. You can increase the time #{Chef::Dist::CLIENT} is given
to complete by configuring windows_service.watchdog_timeout in your client.rb.
EOF
rescue Mixlib::ShellOut::ShellCommandFailed => e
Chef::Log.warn "Not able to start #{Chef::Dist::CLIENT} in new process (#{e})"
rescue => e
Chef::Log.error e
ensure
# Once process exits, we log the current process' pid
Chef::Log.info "Child process exited (pid: #{Process.pid})"
end
def apply_config(config_file_path)
Chef::Config.from_file(config_file_path)
Chef::Config.merge!(config)
end
# Lifted from Chef::Application, with addition of optional startup parameters
# for playing nicely with Windows Services
def reconfigure(startup_parameters = [])
configure_chef startup_parameters
configure_logging
Chef::Config[:chef_server_url] = config[:chef_server_url] if config.key? :chef_server_url
unless Chef::Config[:exception_handlers].any? { |h| Chef::Handler::ErrorReport === h }
Chef::Config[:exception_handlers] << Chef::Handler::ErrorReport.new
end
Chef::Config[:interval] ||= 1800
end
# Lifted from application.rb
# See application.rb for related comments.
def configure_logging
Chef::Log.init(MonoLogger.new(resolve_log_location))
if want_additional_logger?
configure_stdout_logger
end
Chef::Log.level = resolve_log_level
end
def configure_stdout_logger
stdout_logger = MonoLogger.new(STDOUT)
stdout_logger.formatter = Chef::Log.logger.formatter
Chef::Log.loggers << stdout_logger
end
# Based on config and whether or not STDOUT is a tty, should we setup a
# secondary logger for stdout?
def want_additional_logger?
( Chef::Config[:log_location] != STDOUT ) && STDOUT.tty? && !Chef::Config[:daemonize]
end
# Use of output formatters is assumed if `force_formatter` is set or if
# `force_logger` is not set
def using_output_formatter?
Chef::Config[:force_formatter] || !Chef::Config[:force_logger]
end
def auto_log_level?
Chef::Config[:log_level] == :auto
end
def resolve_log_location
# STDOUT is the default log location, but makes no sense for a windows service
Chef::Config[:log_location] == STDOUT ? DEFAULT_LOG_LOCATION : Chef::Config[:log_location]
end
# if log_level is `:auto`, convert it to :warn (when using output formatter)
# or :info (no output formatter). See also +using_output_formatter?+
def resolve_log_level
if auto_log_level?
if using_output_formatter?
:warn
else
:info
end
else
Chef::Config[:log_level]
end
end
def configure_chef(startup_parameters)
# Bit of a hack ahead:
# It is possible to specify a service's binary_path_name with arguments, like "foo.exe -x argX".
# It is also possible to specify startup parameters separately, either via the Services manager
# or by using the registry (I think).
# In order to accommodate all possible sources of parameterization, we first parse any command line
# arguments. We then parse any startup parameters. This works, because Mixlib::CLI reuses its internal
# 'config' hash; thus, anything in startup parameters will override any command line parameters that
# might be set via the service's binary_path_name
#
# All these parameters then get layered on top of those from Chef::Config
parse_options # Operates on ARGV by default
parse_options startup_parameters
begin
case config[:config_file]
when %r{^(http|https)://}
Chef::HTTP.new("").streaming_request(config[:config_file]) { |f| apply_config(f.path) }
else
::File.open(config[:config_file]) { |f| apply_config(f.path) }
end
rescue Errno::ENOENT
Chef::Log.warn("*****************************************")
Chef::Log.warn("Did not find config file: #{config[:config_file]}. Using command line options instead.")
Chef::Log.warn("*****************************************")
Chef::Config.merge!(config)
rescue SocketError
Chef::Application.fatal!("Error getting config file #{Chef::Config[:config_file]}")
rescue Chef::Exceptions::ConfigurationError => error
Chef::Application.fatal!("Error processing config file #{Chef::Config[:config_file]} with error #{error.message}")
rescue Exception => error
Chef::Application.fatal!("Unknown error processing config file #{Chef::Config[:config_file]} with error #{error.message}")
end
end
end
end
end
# To run this file as a service, it must be called as a script from within
# the Windows Service framework. In that case, kick off the main loop!
if __FILE__ == $0
Chef::Application::WindowsService.mainloop
end