summaryrefslogtreecommitdiff
path: root/lib/chef
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2021-02-03 16:47:02 -0800
committerGitHub <noreply@github.com>2021-02-03 16:47:02 -0800
commit0468affb2a4a00784fe8f9535a35301f4c0c7762 (patch)
treea017315f7f53bd2b84bdec5eb4160e4445018cc2 /lib/chef
parentb14a3e76165e243fcabe61db60be80f8c13c36a1 (diff)
parentdecd821184efc47df6f0fb9fcaf07ed3e73a48e2 (diff)
downloadchef-0468affb2a4a00784fe8f9535a35301f4c0c7762.tar.gz
Merge pull request #10928 from chef/service_manager
Signed-off-by: Tim Smith <tsmith@chef.io>
Diffstat (limited to 'lib/chef')
-rw-r--r--lib/chef/application/windows_service.rb338
-rw-r--r--lib/chef/application/windows_service_manager.rb205
2 files changed, 0 insertions, 543 deletions
diff --git a/lib/chef/application/windows_service.rb b/lib/chef/application/windows_service.rb
deleted file mode 100644
index 8975556f75..0000000000
--- a/lib/chef/application/windows_service.rb
+++ /dev/null
@@ -1,338 +0,0 @@
-#
-# 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 "chef-utils/dist" unless defined?(ChefUtils::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 #{ChefUtils::Dist::Infra::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 #{ChefUtils::Dist::Infra::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("#{ChefUtils::Dist::Infra::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
-
- Chef::Log.info("Next #{ChefUtils::Dist::Infra::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("#{ChefUtils::Dist::Infra::CLIENT} service is starting a #{ChefUtils::Dist::Infra::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
-
- # 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 #{ChefUtils::Dist::Infra::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 #{ChefUtils::Dist::Infra::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 #{ChefUtils::Dist::Infra::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 #{ChefUtils::Dist::Infra::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(
- "#{ChefUtils::Dist::Infra::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 "#{ChefUtils::Dist::Infra::CLIENT} timed out\n(#{e})"
- Chef::Log.error(<<-EOF)
- Your #{ChefUtils::Dist::Infra::CLIENT} run timed out. You can increase the time #{ChefUtils::Dist::Infra::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 #{ChefUtils::Dist::Infra::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
diff --git a/lib/chef/application/windows_service_manager.rb b/lib/chef/application/windows_service_manager.rb
deleted file mode 100644
index 4f0de26411..0000000000
--- a/lib/chef/application/windows_service_manager.rb
+++ /dev/null
@@ -1,205 +0,0 @@
-#
-# Author:: Seth Chisamore (<schisamo@chef.io>)
-# 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.
-#
-
-if RUBY_PLATFORM.match?(/mswin|mingw32|windows/)
- require "win32/service"
-end
-require_relative "../config"
-require "mixlib/cli" unless defined?(Mixlib::CLI)
-require "chef-utils/dist" unless defined?(ChefUtils::Dist)
-
-class Chef
- class Application
- #
- # This class is used to create and manage a windows service.
- # Service should be created using Daemon class from
- # win32/service gem.
- # For an example see: Chef::Application::WindowsService
- #
- # Outside programs are expected to use this class to manage
- # windows services.
- #
- class WindowsServiceManager
- include Mixlib::CLI
-
- option :action,
- short: "-a ACTION",
- long: "--action ACTION",
- default: "status",
- description: "Action to carry out on #{ChefUtils::Dist::Infra::SHORT}-service (install, uninstall, status, start, stop, pause, or resume)."
-
- option :config_file,
- short: "-c CONFIG",
- long: "--config CONFIG",
- default: "#{ChefConfig::Config.c_chef_dir}/client.rb",
- description: "The configuration file to use for #{ChefUtils::Dist::Infra::PRODUCT} runs."
-
- option :log_location,
- short: "-L LOGLOCATION",
- long: "--logfile LOGLOCATION",
- description: "Set the log file location for #{ChefUtils::Dist::Infra::SHORT}-service."
-
- option :help,
- short: "-h",
- long: "--help",
- description: "Show this help message.",
- on: :tail,
- boolean: true,
- show_options: true,
- exit: 0
-
- option :version,
- short: "-v",
- long: "--version",
- description: "Show #{ChefUtils::Dist::Infra::PRODUCT} version.",
- boolean: true,
- proc: lambda { |v| puts "#{ChefUtils::Dist::Infra::PRODUCT}: #{::Chef::VERSION}" },
- exit: 0
-
- def initialize(service_options)
- # having to call super in initialize is the most annoying
- # anti-pattern :(
- super()
-
- raise ArgumentError, "Service definition is not provided" if service_options.nil?
-
- required_options = %i{service_name service_display_name service_description service_file_path}
-
- required_options.each do |req_option|
- unless service_options.key?(req_option)
- raise ArgumentError, "Service definition doesn't contain required option #{req_option}"
- end
- end
-
- @service_name = service_options[:service_name]
- @service_display_name = service_options[:service_display_name]
- @service_description = service_options[:service_description]
- @service_file_path = service_options[:service_file_path]
- @service_start_name = service_options[:run_as_user]
- @password = service_options[:run_as_password]
- @delayed_start = service_options[:delayed_start]
- @dependencies = service_options[:dependencies]
- end
-
- def run(params = ARGV)
- parse_options(params)
-
- case config[:action]
- when "install"
- if service_exists?
- puts "Service #{@service_name} already exists on the system."
- else
- ruby = File.join(RbConfig::CONFIG["bindir"], "ruby")
-
- opts = ""
- opts << " -c #{config[:config_file]}" if config[:config_file]
- opts << " -L #{config[:log_location]}" if config[:log_location]
-
- # Quote the full paths to deal with possible spaces in the path name.
- # Also ensure all forward slashes are backslashes
- cmd = "\"#{ruby}\" \"#{@service_file_path}\" #{opts}".gsub(File::SEPARATOR, File::ALT_SEPARATOR)
-
- ::Win32::Service.new(
- service_name: @service_name,
- display_name: @service_display_name,
- description: @service_description,
- # Prior to 0.8.5, win32-service creates interactive services by default,
- # and we don't want that, so we need to override the service type.
- service_type: ::Win32::Service::SERVICE_WIN32_OWN_PROCESS,
- start_type: ::Win32::Service::SERVICE_AUTO_START,
- binary_path_name: cmd,
- service_start_name: @service_start_name,
- password: @password,
- dependencies: @dependencies
- )
- unless @delayed_start.nil?
- ::Win32::Service.configure(
- service_name: @service_name,
- delayed_start: @delayed_start
- )
- end
- puts "Service '#{@service_name}' has successfully been installed."
- end
- when "status"
- if !service_exists?
- puts "Service #{@service_name} doesn't exist on the system."
- else
- puts "State of #{@service_name} service is: #{current_state}"
- end
- when "start"
- # TODO: allow override of startup parameters here?
- take_action("start", RUNNING)
- when "stop"
- take_action("stop", STOPPED)
- when "uninstall", "delete"
- take_action("stop", STOPPED)
- unless service_exists?
- puts "Service #{@service_name} doesn't exist on the system."
- else
- ::Win32::Service.delete(@service_name)
- puts "Service #{@service_name} deleted"
- end
- when "pause"
- take_action("pause", PAUSED)
- when "resume"
- take_action("resume", RUNNING)
- end
- end
-
- private
-
- # Just some state constants
- STOPPED = "stopped".freeze
- RUNNING = "running".freeze
- PAUSED = "paused".freeze
-
- def service_exists?
- ::Win32::Service.exists?(@service_name)
- end
-
- def take_action(action = nil, desired_state = nil)
- if service_exists?
- if current_state != desired_state
- ::Win32::Service.send(action, @service_name)
- wait_for_state(desired_state)
- puts "Service '#{@service_name}' is now '#{current_state}'."
- else
- puts "Service '#{@service_name}' is already '#{desired_state}'."
- end
- else
- puts "Cannot '#{action}' service '#{@service_name}'"
- puts "Service #{@service_name} doesn't exist on the system."
- end
- end
-
- def current_state
- ::Win32::Service.status(@service_name).current_state
- end
-
- # Helper method that waits for a status to change its state since state
- # changes aren't usually instantaneous.
- def wait_for_state(desired_state)
- while current_state != desired_state
- puts "One moment... #{current_state}"
- sleep 1
- end
- end
-
- end
- end
-end