class Orocos::Process
The representation of an Orocos process. It manages starting the process and cleaning up when the process dies.
Constants
- CommandLine
- SIGNAL_NUMBERS
- VALID_LOG_LEVELS
Attributes
A map of existing running processes
@return [{Integer=>Process}] a map from process IDs to the
corresponding Process object
The path to the binary file
The component process ID
Public Class Methods
# File lib/orocos/process.rb, line 872 def self.allocate_gdb_port @@gdb_port += 1 end
Deregisters a process object that was registered with {register}
@param [Integer] pid the process PID
# File lib/orocos/process.rb, line 385 def self.deregister(pid) if process = registered_processes.delete(pid) process else raise ArgumentError, "no process registered for PID #{pid}" end end
Enumerates all registered processes
@yieldparam [Process] process the process object
# File lib/orocos/process.rb, line 395 def self.each(&block) registered_processes.each_value(&block) end
Returns the process that has the given PID
@param [Integer] pid the PID whose process we are looking for @return [nil,Process] the process object whose PID matches, or nil
# File lib/orocos/process.rb, line 352 def self.from_pid(pid) if result = registered_processes[pid] return result end end
# File lib/orocos/process.rb, line 867 def self.gdb_base_port=(port) @@gdb_port = port - 1 end
@api private
Checks that the given command can be resolved
# File lib/orocos/process.rb, line 697 def self.has_command?(cmd) if File.file?(cmd) && File.executable?(cmd) return else system("which #{cmd} > /dev/null 2>&1") end end
Kills the given processes
@param [Array<#kill,#join>] processes a list of processes to kill @param [Boolean] wait whether the method should wait for the processes
to die or not
# File lib/orocos/process.rb, line 860 def self.kill(processes, wait = true) processes.each { |p| p.kill if p.running? } if wait processes.each { |p| p.join } end end
Creates a new Process instance which will be able to start and supervise the execution of the given Orocos component
@param [String] name the process name @param [OroGen::Spec::Deployment] model the process deployment'
@overload initialize(name, model_name = name)
deprecated form @param [String] name the process name @param [String] model_name the name of the deployment model
# File lib/orocos/process.rb, line 423 def initialize(name, model = name, loader: Orocos.default_pkgconfig_loader, name_mappings: Hash.new) model = if model.respond_to?(:to_str) loader.deployment_model_from_name(model) else model end @binfile = if loader.respond_to?(:find_deployment_binfile) loader.find_deployment_binfile(model.name) else loader.available_deployments[model.name].binfile end super(name, model, name_mappings: name_mappings) end
@api private
Apply default parameters for the wait option in {Orocos.run} and {#spawn}
# File lib/orocos/process.rb, line 571 def self.normalize_wait_option(wait, valgrind, gdb) if wait.nil? wait = if valgrind then 600 elsif gdb then 600 else 20 end elsif !wait false else wait end end
@api private
Normalizes the options for command line wrappers such as gdb and valgrind as passed to {Orocos.run}
@overload ::parse_cmdline_wrapper_option(cmd, enable, cmd_options, deployments)
@param [String] cmd the wrapper command string @param [Boolean] enable whether the wrapper should be enabled or not @param [Hash] options additional options to pass to the wrapper @param [Array<String>] deployments the deployments on which the wrapper should be activated
@overload ::parse_cmdline_wrapper_option(cmd, deployments, cmd_options, all_deployments)
@param [String] cmd the wrapper command string @param [Array<String>] deployments the name of the deployments on which the wrapper should be used. @param [Hash] options additional options to pass to the wrapper @param [Array<String>] all_deployments ignored in this form
@overload ::parse_cmdline_wrapper_option(cmd, deployments_to_cmd_options, cmd_options, all_deployments)
@param [String] cmd the wrapper command string @param [Hash<String,Array<String>>] deployments_to_cmd_options mapping from name of deployments to the list of additional options that should be passed to the wrapper @param [Hash] options additional options to pass to the wrapper @param [Array<String>] all_deployments ignored in this form
# File lib/orocos/process.rb, line 732 def self.parse_cmdline_wrapper_option(cmd, deployments, options, all_deployments) if !deployments return Hash.new end if !has_command?(cmd) raise "'#{cmd}' option is specified, but #{cmd} seems not to be installed" end if !deployments.respond_to?(:to_hash) if deployments.respond_to?(:to_str) deployments = [deployments] elsif !deployments.respond_to?(:to_ary) deployments = all_deployments end deployments = deployments.inject(Hash.new) { |h, name| h[name] = options; h } end deployments.each_key do |name| if !all_deployments.include?(name) raise ArgumentError, "#{name}, selected to be executed under #{cmd}, is not a known deployment/model" end end end
@api private
Normalizes the log_level option passed to {Orocos.run}.
@param [Hash,Symbol] options is given as a symbol, this is the log level that should be applied to all deployments. Otherwise, it is a hash from a process name to the log level that should be applied for this particular deployment @param [Array<String>] all_deployments the name of all deployments @return [Hash<String,Symbol>] a hash from a name in all_deployments to
the log level for that deployment
# File lib/orocos/process.rb, line 686 def self.parse_log_level_option( options, all_deployments ) if !options.respond_to?(:to_hash) all_deployments.inject(Hash.new) { |h, name| h[name] = options; h } else options end end
@api private
Separate the list of deployments from the spawn options in options passed to {Orocos.run}
Valid options are: @param [Boolean,Numeric] wait
wait that number of seconds (can be floating-point) for the processes to be ready. If it did not start into the provided timeout, an Orocos::NotFound exception raised. nil enables waiting for a predefined number of seconds that depend on the usage or not of valgrind and gdb. false disables waiting completely, and true waits forever.
@param [String] output
redirect the process output to the given file. The %m and %p patterns will be replaced by respectively the name and the PID of each process.
@param [Boolean,Array<String>] valgrind
start some or all the processes under valgrind. It can either be an array of process names (e.g. valgrind: ['p1', 'p2']) or 'true'. In the first case, the listed processes will be added to the list of processes to start (if they are not already in it) and will be started under valgrind. In the second case, all processes are started under valgrind.
@param [Array<String>] valgrind_options
an array of options that should be passed to valgrind, e.g. valgrind_options: ["--track-origins=yes"]
@param [Boolean,Array<String>] gdb
start some or all the processes under gdbserver. It can either be an array of process names (e.g. gdbserver: ['p1', 'p2']) or 'true'. In the first case, the listed processes will be added to the list of processes to start (if they are not already in it) and will be started under gdbserver. In the second case, all processes are started under gdbserver.
@param [Array<String>] gdb_options
an array of options that should be passed to gdbserver
@param [Hash<String>] cmdline_args
When command line arguments are available to deployments, they can be
set using the following option:
cmdline_args: { "sd-domain" => '_robot._tcp', "prefix" => "test" }
This will be mapped to '--sd-domain=_robot._tcp --prefix=test'
One notable command line argument is --sd-domain
The service discovery domain in which this process should be published
This is only supported by deployments and orogen if the service_discovery
package has been installed along with orogen
The sd domain is of the format: <name>.<suffix> where the suffix has to
be one of _tcp or _udp
@return [(Array<String,Hash,String,Hash>,Object)] the first returned
element is a list of (deployment_name, name_mappings, process_name, spawn_options) tuples. The second element is the wait option (either a Numeric or false)
# File lib/orocos/process.rb, line 638 def self.parse_run_options(*names, wait: nil, loader: Orocos.default_loader, valgrind: false, valgrind_options: Hash.new, gdb: false, gdb_options: Hash.new, log_level: nil, output: nil, oro_logfile: "orocos.%m-%p.txt", working_directory: Orocos.default_working_directory, cmdline_args: Hash.new) deployments, models = partition_run_options(*names, loader: loader) wait = normalize_wait_option(wait, valgrind, gdb) all_deployments = deployments.keys.map(&:name) + models.values.flatten valgrind = parse_cmdline_wrapper_option( 'valgrind', valgrind, valgrind_options, all_deployments) gdb = parse_cmdline_wrapper_option( 'gdbserver', gdb, gdb_options, all_deployments) log_level = parse_log_level_option(log_level, all_deployments) name_mappings = resolve_name_mappings(deployments, models) processes = name_mappings.map do |deployment_name, mappings, name| output = if output output.gsub '%m', name end spawn_options = Hash[ working_directory: working_directory, output: output, valgrind: valgrind[name], gdb: gdb[name], cmdline_args: cmdline_args, wait: false, log_level: log_level[name], oro_logfile: oro_logfile] [deployment_name, mappings, name, spawn_options] end return processes, wait end
Converts the options given to Orocos.run in a more normalized format
It returns a triple (deployments, models, options) where
-
c deployments is a map from a deployment name to a prefix that should be used to run this deployment. Prefixes are prepended to all task names in the deployment. It is set to nil if there are no prefix.
-
c models is a mapping from a oroGen model name to a name. It requests to start the default deployment for the model_name, using c name as the task name
-
options are options that should be passed to spawn
For instance, in
Orocos.run 'xsens', 'xsens_imu::Task' => 'imu', :valgrind => true
One deployment called 'xsens' should be called with no prefix, the default deployment for xsens_imu::Task should be started and the corresponding task be renamed to 'imu' and all deployments should be started with the :valgrind => true option. Therefore, the parsed options would be
deployments = { 'xsens' => nil } models = { 'xsens_imu::Task' => 'imu' } options = { valgrind => true }
In case multiple instances of a single model need to be started, the names can be given as an Array. E.g.
Orocos.run 'xsens_imu::Task' => ['imu1', 'imu2']
# File lib/orocos/process.rb, line 529 def self.partition_run_options(*names, loader: Orocos.default_loader) mapped_names = Hash.new if names.last.kind_of?(Hash) mapped_names = names.pop end deployments, models = Hash.new, Hash.new names.each { |n| mapped_names[n] = nil } mapped_names.each do |object, new_name| # If given a name, resolve to the corresponding oroGen spec # object if object.respond_to?(:to_str) || object.respond_to?(:to_sym) object = object.to_s begin object = loader.task_model_from_name(object) rescue OroGen::NotFound begin object = loader.deployment_model_from_name(object) rescue OroGen::NotFound raise OroGen::NotFound, "#{object} is neither a task model nor a deployment name" end end end case object when OroGen::Spec::TaskContext if !new_name raise TaskNameRequired, "you must provide a task name when starting a component by type, as e.g. Orocos.run 'xsens_imu::Task' => 'xsens'" end models[object] = Array(new_name) when OroGen::Spec::Deployment deployments[object] = (new_name if new_name) else raise ArgumentError, "expected a task context model or a deployment model, got #{object}" end end return deployments, models end
Registers a PID-to-process mapping.
This can be called only for running processes
@param [Process] process the process that should be registered @return [void] @see deregister each
# File lib/orocos/process.rb, line 374 def self.register(process) if !process.alive? raise ArgumentError, "cannot register a non-running process" end registered_processes[process.pid] = process nil end
# File lib/orocos/process.rb, line 1149 def self.resolve_all_tasks(process, cache = Hash.new) # Get any task name from that specific deployment, and check we # can access it. If there is none all_reachable = process.task_names.all? do |task_name| begin cache[task_name] ||= yield(task_name) rescue Orocos::NotFound end end if all_reachable cache end end
@api private
Resolve the 'prefix' options given to {Orocos.run} into an exhaustive task name mapping
@param [Array<(OroGen::Spec::Deployment,String)>] deployments the list
of deployments that should be started along with a prefix string that should be prepended to the deployment's tasks
@param [Array<(OroGen::Spec::TaskContext,String)>] models a list of
task context models that should be deployed, along with the task name that should be used for these models.
@return [Array<(String,Hash,String)>] a tuple of the name of a binary,
the name mappings that should be used when spawning this binary and the desired process name.
# File lib/orocos/process.rb, line 771 def self.resolve_name_mappings(deployments, models) processes = [] processes += deployments.map do |deployment, prefix| mapped_name = deployment.name name_mappings = Hash.new if prefix name_mappings = ProcessBase.resolve_prefix(deployment, prefix) mapped_name = "#{prefix}#{deployment.name}" end [deployment.name, name_mappings, mapped_name] end models.each do |model, desired_names| desired_names = [desired_names] unless desired_names.kind_of? Array desired_names.each do |desired_name| process_name = OroGen::Spec::Project.default_deployment_name(model.name) name_mappings = Hash[ process_name => desired_name, "#{process_name}_Logger" => "#{desired_name}_Logger"] processes << [process_name, name_mappings, desired_name] end end processes end
@deprecated use {Orocos.run} directly instead
# File lib/orocos/process.rb, line 798 def self.run(*args, **options) if !Orocos.initialized? #try to initialize orocos before bothering the user Orocos.initialize end if !Orocos::CORBA.initialized? raise "CORBA layer is not initialized! There might be problem with the installation." end begin process_specs, wait = parse_run_options(*args, **options) # Then spawn them, but without waiting for them processes = process_specs.map do |deployment_name, name_mappings, name, spawn_options| p = Process.new(name, deployment_name) name_mappings.each do |old, new| p.map_name old, new end p.spawn(spawn_options) p end # Finally, if the user required it, wait for the processes to run if wait timeout = if wait.kind_of?(Numeric) wait else Float::INFINITY end processes.each { |p| p.wait_running(timeout) } end rescue Exception => original_error # Kill the processes that are already running if processes begin kill(processes.map { |p| p if p.running? }.compact) rescue Exception => e Orocos.warn "failed to kill the started processes, you will have to kill them yourself" Orocos.warn e.message e.backtrace.each do |l| Orocos.warn " #{l}" end raise original_error end end raise end if block_given? Orocos.guard(*processes) do yield(*processes) end else processes end end
Tries to stop and cleanup the provided task. Returns true if it was successful, and false otherwise
# File lib/orocos/process.rb, line 1243 def self.try_task_cleanup(task) begin task.stop(false) if task.model && task.model.needs_configuration? task.cleanup(false) end rescue StateTransitionFailed end task.each_port do |port| port.disconnect_all end true rescue Exception => e Orocos.warn "clean shutdown of #{task.name} failed: #{e.message}" e.backtrace.each do |line| Orocos.warn line end false end
Wait for a process to become reachable
# File lib/orocos/process.rb, line 1165 def self.wait_running(process, timeout = nil, name_service = Orocos::CORBA.name_service, &block) if timeout == 0 return nil if !process.alive? # Use custom block to check if the process is reachable if block_given? block.call(process) else # Get any task name from that specific deployment, and check we # can access it. If there is none all_reachable = process.task_names.all? do |task_name| if name_service.task_reachable?(task_name) Orocos.debug "#{task_name} is reachable" true else Orocos.debug "could not access #{task_name}, #{name} is not running yet ..." false end end if all_reachable Orocos.info "all tasks of #{process.name} are reachable, assuming it is up and running" end all_reachable end else start_time = Time.now got_alive = process.alive? while true if wait_running(process, 0, name_service, &block) break elsif not timeout break elsif timeout < Time.now - start_time break end if got_alive && !process.alive? raise Orocos::NotFound, "#{process.name} was started but crashed" end sleep 0.1 end if process.alive? return true else raise Orocos::NotFound, "cannot get a running #{process.name} module" end end end
Public Instance Methods
True if the process is running
# File lib/orocos/process.rb, line 455 def alive?; !!@pid end
Massages various spawn parameters into the actual deployment command line
@return [CommandLine]
# File lib/orocos/process.rb, line 883 def command_line(working_directory: Orocos.default_working_directory, log_level: nil, cmdline_args: Hash.new, tracing: Orocos.tracing?, gdb: nil, valgrind: nil, name_service_ip: Orocos::CORBA.name_service_ip) result = CommandLine.new(Hash.new, nil, [], working_directory) result.command = binfile if tracing result.env['LD_PRELOAD'] = Orocos.tracing_library_path end if log_level valid_levels = [:debug, :info, :warn, :error, :fatal, :disable] if valid_levels.include?(log_level) result.env['BASE_LOG_LEVEL'] = log_level.to_s.upcase else raise ArgumentError, "'#{log_level}' is not a valid log level." + " Valid options are #{valid_levels}." end end if name_service_ip result.env['ORBInitRef'] = "NameService=corbaname::#{name_service_ip}" end cmdline_args = cmdline_args.dup cmdline_args[:rename] ||= [] name_mappings.each do |old, new| cmdline_args[:rename].push "#{old}:#{new}" end # Command line arguments have to be of type --<option>=<value> # or if <value> is nil a valueless option, i.e. --<option> cmdline_args.each do |option, value| if value if value.respond_to?(:to_ary) value.each do |v| result.args.push "--#{option}=#{v}" end else result.args.push "--#{option}=#{value}" end else result.args.push "--#{option}" end end if gdb result.args.unshift(result.command) if gdb == true gdb = Process.allocate_gdb_port end result.args.unshift("0.0.0.0:#{gdb}") result.command = 'gdbserver' elsif valgrind result.args.unshift(result.command) result.command = 'valgrind' end return result end
A string describing the host. It can be used to check if two processes are running on the same host
# File lib/orocos/process.rb, line 401 def host_id 'localhost' end
Waits until the process dies
This is valid only if the module has been started under Orocos supervision, using {#spawn}
# File lib/orocos/process.rb, line 443 def join return unless alive? begin ::Process.waitpid(pid) exit_status = $? dead!(exit_status) rescue Errno::ECHILD end end
Kills the process either cleanly by requesting a shutdown if signal == nil, or forcefully by using UNIX signals if signal is a signal name.
# File lib/orocos/process.rb, line 1268 def kill(wait = true, signal = nil) tpid = pid return if !tpid # already dead # Stop all tasks and disconnect the ports if !signal clean_shutdown = true begin each_task do |task| if !self.class.try_task_cleanup(task) clean_shutdown = false break end end rescue Orocos::NotFound, Orocos::NoModel # We're probably still starting the process. Just go on and # signal it clean_shutdown = false end if !clean_shutdown Orocos.warn "clean shutdown of process #{name} failed" end end expected_exit = nil if clean_shutdown expected_exit = signal = SIGNAL_NUMBERS['SIGINT'] else signal = SIGNAL_NUMBERS['SIGINT'] end if signal if !expected_exit Orocos.warn "sending #{signal} to #{name}" end if signal.respond_to?(:to_str) && signal !~ /^SIG/ signal = "SIG#{signal}" end expected_exit ||= if signal.kind_of?(Integer) then signal else SIGNAL_NUMBERS[signal] || signal end @expected_exit = expected_exit begin ::Process.kill(signal, tpid) rescue Errno::ESRCH # Already exited return end end if wait join if @exit_status && @exit_status.signaled? if !expected_exit Orocos.warn "#{name} unexpectedly exited with signal #{@exit_status.termsig}" elsif @exit_status.termsig != expected_exit Orocos.warn "#{name} was expected to quit with signal #{expected_exit} but terminated with signal #{@exit_status.termsig}" end end end end
Returns true if the process is located on the same host than the Ruby interpreter
# File lib/orocos/process.rb, line 407 def on_localhost? host_id == 'localhost' end
# File lib/orocos/process.rb, line 1215 def resolve_all_tasks(cache = Hash.new, name_service: Orocos::CORBA.name_service) Process.resolve_all_tasks(self, cache) do |task_name| name_service.get(task_name) end end
True if the process is running
# File lib/orocos/process.rb, line 457 def running?; alive? end
Spawns this process
@param [Symbol] log_level the log level under which the process should
be run. Must be one of {VALID_LOG_LEVELS}
@param [String] working_directory the working directory @param [String] oro_logfile the name of the RTT-generated logfile.
%m will be replaced by the process' name and %p by its PID
@param [String] prefix a prefix that should be prepended to all tasks
in the process
@param [Boolean] tracing whether the tracing library
{Orocos.tracing_library_path} should be preloaded before executing the
process
@param [#get] name_service a name service object that should be used
to resolve the tasks
@param [Boolean,Numeric] wait if true, the method will wait forever
for the tasks to be available. If false, it will not wait at all. If nil, a sane default will be used (the default depends on whether the process is executed under valgrind or gdb). Finally, if a numerical value is provided, this value will be used as timeout (in seconds)
@param [Boolean,Array<String>] gdb whether the process should be
executed under the supervision of gdbserver. Setting this option to true will enable gdb support. Setting it to an array of strings will specify a list of arguments that should be passed to gdbserver. This is obviously incompatible with the valgrind option. A warning message is issued, that describes how to connect to the gdbserver instance.
@param [Boolean,Array<String>] valgrind whether the process should be
executed under the supervision of valgrind. Setting this option to true will enable valgrind support. Setting it to an array of strings will specify a list of arguments that should be passed to valgrind itself. This is obviously incompatible with the gdb option.
# File lib/orocos/process.rb, line 977 def spawn(log_level: nil, working_directory: Orocos.default_working_directory, cmdline_args: Hash.new, oro_logfile: "orocos.%m-%p.txt", prefix: nil, tracing: Orocos.tracing?, name_service: Orocos::CORBA.name_service, wait: nil, output: nil, gdb: nil, valgrind: nil) raise "#{name} is already running" if alive? Orocos.info "starting deployment #{name}" # Setup mapping for prefixed tasks in Process class prefix_mappings = ProcessBase.resolve_prefix(model, prefix) name_mappings = prefix_mappings.merge(self.name_mappings) self.name_mappings = name_mappings # If possible, check that we won't clash with an already running # process task_names.each do |name| if name_service.task_reachable?(name) raise ArgumentError, "there is already a running task called #{name}, are you starting the same component twice ?" end end if wait.nil? wait = if valgrind then 600 elsif gdb then 600 else 20 end end cmdline_args = cmdline_args.dup cmdline_args[:rename] ||= [] name_mappings.each do |old, new| cmdline_args[:rename].push "#{old}:#{new}" end if valgrind cmdline_wrapper = 'valgrind' cmdline_wrapper_options = if valgrind.respond_to?(:to_ary) valgrind else [] end elsif gdb cmdline_wrapper = 'gdbserver' cmdline_wrapper_options = if gdb.respond_to?(:to_ary) gdb else [] end gdb_port = Process.allocate_gdb_port cmdline_wrapper_options << "localhost:#{gdb_port}" end if !name_service.ip.empty? ENV['ORBInitRef'] = "NameService=corbaname::#{name_service.ip}" end cmdline = [binfile] # check arguments for log_level if log_level valid_levels = [:debug, :info, :warn, :error, :fatal, :disable] if valid_levels.include?(log_level) log_level = log_level.to_s.upcase else raise ArgumentError, "'#{log_level}' is not a valid log level." + " Valid options are #{valid_levels}." end end read, write = IO.pipe @pid = fork do if tracing ENV['LD_PRELOAD'] = Orocos.tracing_library_path end pid = ::Process.pid real_name = get_mapped_name(name) ENV['BASE_LOG_LEVEL'] = log_level if log_level if output && output.respond_to?(:to_str) output_file_name = output. gsub('%m', real_name). gsub('%p', pid.to_s) if working_directory output_file_name = File.expand_path(output_file_name, working_directory) end output = File.open(output_file_name, 'a') end if oro_logfile oro_logfile = oro_logfile. gsub('%m', real_name). gsub('%p', pid.to_s) if working_directory oro_logfile = File.expand_path(oro_logfile, working_directory) end ENV['ORO_LOGFILE'] = oro_logfile else ENV['ORO_LOGFILE'] = "/dev/null" end if output STDERR.reopen(output) STDOUT.reopen(output) end if output_file_name && valgrind cmdline.unshift "--log-file=#{output_file_name}.valgrind" end if cmdline_wrapper cmdline = cmdline_wrapper_options + cmdline cmdline.unshift cmdline_wrapper end # Command line arguments have to be of type --<option>=<value> # or if <value> is nil a valueless option, i.e. --<option> if cmdline_args cmdline_args.each do |option, value| if value if value.respond_to?(:to_ary) value.each do |v| cmdline.push "--#{option}=#{v}" end else cmdline.push "--#{option}=#{value}" end else cmdline.push "--#{option}" end end end read.close write.fcntl(Fcntl::F_SETFD, 1) ::Process.setpgrp begin if working_directory Dir.chdir(working_directory) end exec(*cmdline) rescue Exception write.write("FAILED") end end Process.register(self) write.close if read.read == "FAILED" raise "cannot start #{name}" end if gdb Orocos.warn "process #{name} has been started under gdbserver, port=#{gdb_port}. The components will not be functional until you attach a GDB to the started server" end if wait timeout = if wait.kind_of?(Numeric) wait elsif wait Float::INFINITY end wait_running(timeout, name_service) end end
Wait for the module to be started. If timeout is 0, the function returns immediately, with a false return value if the module is not started yet and a true return value if it is started.
Otherwise, it waits for the process to start for the specified amount of seconds. It will throw Orocos::NotFound if the process was not started within that time.
If timeout is nil, the method will wait indefinitely
# File lib/orocos/process.rb, line 1230 def wait_running(timeout = nil, name_service = Orocos::CORBA.name_service) Process.wait_running(self, timeout, name_service) end