Merge remote-tracking branch 'daniel-pittman/refactor/2.7.x/4862-remove-the-event-loop-library' into 2.7.x
* daniel-pittman/refactor/2.7.x/4862-remove-the-event-loop-library: (#4862) Finally remove the event-loop library. (#4862) define_method is not a public method in Ruby. (#4862) `returning` is not a standard Ruby method. (#4862) Stop using EventLoop in the Puppet daemon.
This commit is contained in:
Коммит
75476e1ae2
|
@ -1,5 +1,4 @@
|
|||
require 'sync'
|
||||
require 'puppet/external/event-loop'
|
||||
require 'puppet/application'
|
||||
|
||||
# A general class for triggering a run of another
|
||||
|
@ -75,18 +74,6 @@ class Puppet::Agent
|
|||
@splayed = true
|
||||
end
|
||||
|
||||
# Start listening for events. We're pretty much just listening for
|
||||
# timer events here.
|
||||
def start
|
||||
# Create our timer. Puppet will handle observing it and such.
|
||||
timer = EventLoop::Timer.new(:interval => Puppet[:runinterval], :tolerance => 1, :start? => true) do
|
||||
run
|
||||
end
|
||||
|
||||
# Run once before we start following the timer
|
||||
timer.sound_alarm
|
||||
end
|
||||
|
||||
def sync
|
||||
@sync ||= Sync.new
|
||||
end
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
require 'puppet'
|
||||
require 'puppet/util/pidlock'
|
||||
require 'puppet/external/event-loop'
|
||||
require 'puppet/application'
|
||||
|
||||
# A module that handles operations common to all daemons. This is included
|
||||
|
@ -120,10 +119,77 @@ class Puppet::Daemon
|
|||
create_pidfile
|
||||
|
||||
raise Puppet::DevError, "Daemons must have an agent, server, or both" unless agent or server
|
||||
server.start if server
|
||||
agent.start if agent
|
||||
|
||||
EventLoop.current.run
|
||||
# Start the listening server, if required.
|
||||
server.start if server
|
||||
|
||||
# Finally, loop forever running events - or, at least, until we exit.
|
||||
run_event_loop
|
||||
end
|
||||
|
||||
def run_event_loop
|
||||
# Now, we loop waiting for either the configuration file to change, or the
|
||||
# next agent run to be due. Fun times.
|
||||
#
|
||||
# We want to trigger the reparse if 15 seconds passed since the previous
|
||||
# wakeup, and the agent run if Puppet[:runinterval] seconds have passed
|
||||
# since the previous wakeup.
|
||||
#
|
||||
# We always want to run the agent on startup, so it was always before now.
|
||||
# Because 0 means "continuously run", `to_i` does the right thing when the
|
||||
# input is strange or badly formed by returning 0. Integer will raise,
|
||||
# which we don't want, and we want to protect against -1 or below.
|
||||
next_agent_run = 0
|
||||
agent_run_interval = [Puppet[:runinterval].to_i, 0].max
|
||||
|
||||
# We may not want to reparse; that can be disable. Fun times.
|
||||
next_reparse = 0
|
||||
reparse_interval = Puppet[:filetimeout].to_i
|
||||
|
||||
loop do
|
||||
now = Time.now.to_i
|
||||
|
||||
# Handle reparsing of configuration files, if desired and required.
|
||||
# `reparse` will just check if the action is required, and would be
|
||||
# better named `reparse_if_changed` instead.
|
||||
if reparse_interval > 0 and now >= next_reparse
|
||||
Puppet.settings.reparse
|
||||
|
||||
# The time to the next reparse might have changed, so recalculate
|
||||
# now. That way we react dynamically to reconfiguration.
|
||||
reparse_interval = Puppet[:filetimeout].to_i
|
||||
next_reparse = now + reparse_interval
|
||||
|
||||
# We should also recalculate the agent run interval, and adjust the
|
||||
# next time it is scheduled to run, just in case. In the event that
|
||||
# we made no change the result will be a zero second adjustment.
|
||||
new_run_interval = [Puppet[:runinterval].to_i, 0].max
|
||||
next_agent_run += agent_run_interval - new_run_interval
|
||||
agent_run_interval = new_run_interval
|
||||
end
|
||||
|
||||
# Handle triggering another agent run. This will block the next check
|
||||
# for configuration reparsing, which is a desired and deliberate
|
||||
# behaviour. You should not change that. --daniel 2012-02-21
|
||||
if agent and now >= next_agent_run
|
||||
agent.run
|
||||
next_agent_run = now + agent_run_interval
|
||||
end
|
||||
|
||||
# Finally, an interruptable able sleep until the next scheduled event.
|
||||
# We also set a default wakeup of "one hour from now", which will
|
||||
# recheck everything at a minimum every hour. Just in case something in
|
||||
# the math messes up or something; it should be inexpensive enough to
|
||||
# wake once an hour, then go back to sleep after doing nothing, if
|
||||
# someone only wants listen mode.
|
||||
next_event = now + 60 * 60
|
||||
next_event > next_reparse and next_event = next_reparse
|
||||
next_event > next_agent_run and next_event = next_agent_run
|
||||
|
||||
how_long = next_event - now
|
||||
|
||||
how_long > 0 and select([], [], [], how_long)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
require "puppet/external/event-loop/event-loop"
|
|
@ -1,367 +0,0 @@
|
|||
## better-definers.rb --- better attribute and method definers
|
||||
# Copyright (C) 2005 Daniel Brockman
|
||||
|
||||
# This program is free software; you can redistribute it
|
||||
# and/or modify it under the terms of the GNU General Public
|
||||
# License as published by the Free Software Foundation;
|
||||
# either version 2 of the License, or (at your option) any
|
||||
# later version.
|
||||
|
||||
# This file is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public
|
||||
# License along with this program; if not, write to the Free
|
||||
# Software Foundation, 51 Franklin Street, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
class Symbol
|
||||
def predicate?
|
||||
to_s.include? "?" end
|
||||
def imperative?
|
||||
to_s.include? "!" end
|
||||
def writer?
|
||||
to_s.include? "=" end
|
||||
|
||||
def punctuated?
|
||||
predicate? or imperative? or writer? end
|
||||
def without_punctuation
|
||||
to_s.delete("?!=").to_sym end
|
||||
|
||||
def predicate
|
||||
without_punctuation.to_s + "?" end
|
||||
def imperative
|
||||
without_punctuation.to_s + "!" end
|
||||
def writer
|
||||
without_punctuation.to_s + "=" end
|
||||
end
|
||||
|
||||
class Hash
|
||||
def collect! (&block)
|
||||
replace Hash[*collect(&block).flatten]
|
||||
end
|
||||
|
||||
def flatten
|
||||
to_a.flatten
|
||||
end
|
||||
end
|
||||
|
||||
module Kernel
|
||||
def returning (value)
|
||||
yield value ; value
|
||||
end
|
||||
end
|
||||
|
||||
class Module
|
||||
def define_hard_aliases (name_pairs)
|
||||
for new_aliases, existing_name in name_pairs do
|
||||
new_aliases.kind_of? Array or new_aliases = [new_aliases]
|
||||
for new_alias in new_aliases do
|
||||
alias_method(new_alias, existing_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def define_soft_aliases (name_pairs)
|
||||
for new_aliases, existing_name in name_pairs do
|
||||
new_aliases.kind_of? Array or new_aliases = [new_aliases]
|
||||
for new_alias in new_aliases do
|
||||
class_eval %{def #{new_alias}(*args, &block)
|
||||
#{existing_name}(*args, &block) end}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
define_soft_aliases \
|
||||
:define_hard_alias => :define_hard_aliases,
|
||||
:define_soft_alias => :define_soft_aliases
|
||||
|
||||
# This method lets you define predicates like :foo?,
|
||||
# which will be defined to return the value of @foo.
|
||||
def define_readers (*names)
|
||||
for name in names.map { |x| x.to_sym } do
|
||||
if name.punctuated?
|
||||
# There's no way to define an efficient reader whose
|
||||
# name is different from the instance variable.
|
||||
class_eval %{def #{name} ; @#{name.without_punctuation} end}
|
||||
else
|
||||
# Use `attr_reader' to define an efficient method.
|
||||
attr_reader(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def writer_defined? (name)
|
||||
method_defined?(name.to_sym.writer)
|
||||
end
|
||||
|
||||
# If you pass a predicate symbol :foo? to this method, it'll first
|
||||
# define a regular writer method :foo, without a question mark.
|
||||
# Then it'll define an imperative writer method :foo! as a shorthand
|
||||
# for setting the property to true.
|
||||
def define_writers (*names, &body)
|
||||
for name in names.map { |x| x.to_sym } do
|
||||
if block_given?
|
||||
define_method(name.writer, &body)
|
||||
else
|
||||
attr_writer(name.without_punctuation)
|
||||
end
|
||||
if name.predicate?
|
||||
class_eval %{def #{name.imperative}
|
||||
self.#{name.writer} true end}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
define_soft_aliases \
|
||||
:define_reader => :define_readers,
|
||||
:define_writer => :define_writers
|
||||
|
||||
# We don't need a singular alias for `define_accessors',
|
||||
# because it always defines at least two methods.
|
||||
|
||||
def define_accessors (*names)
|
||||
define_readers(*names)
|
||||
define_writers(*names)
|
||||
end
|
||||
|
||||
def define_opposite_readers (name_pairs)
|
||||
name_pairs.collect! { |k, v| [k.to_sym, v.to_sym] }
|
||||
for opposite_name, name in name_pairs do
|
||||
define_reader(name) unless method_defined?(name)
|
||||
class_eval %{def #{opposite_name} ; not #{name} end}
|
||||
end
|
||||
end
|
||||
|
||||
def define_opposite_writers (name_pairs)
|
||||
name_pairs.collect! { |k, v| [k.to_sym, v.to_sym] }
|
||||
for opposite_name, name in name_pairs do
|
||||
define_writer(name) unless writer_defined?(name)
|
||||
class_eval %{def #{opposite_name.writer} x
|
||||
self.#{name.writer} !x end}
|
||||
class_eval %{def #{opposite_name.imperative}
|
||||
self.#{name.writer} false end}
|
||||
end
|
||||
end
|
||||
|
||||
define_soft_aliases \
|
||||
:define_opposite_reader => :define_opposite_readers,
|
||||
:define_opposite_writer => :define_opposite_writers
|
||||
|
||||
def define_opposite_accessors (name_pairs)
|
||||
define_opposite_readers name_pairs
|
||||
define_opposite_writers name_pairs
|
||||
end
|
||||
|
||||
def define_reader_with_opposite (name_pair, &body)
|
||||
name, opposite_name = name_pair.flatten.collect { |x| x.to_sym }
|
||||
define_method(name, &body)
|
||||
define_opposite_reader(opposite_name => name)
|
||||
end
|
||||
|
||||
def define_writer_with_opposite (name_pair, &body)
|
||||
name, opposite_name = name_pair.flatten.collect { |x| x.to_sym }
|
||||
define_writer(name, &body)
|
||||
define_opposite_writer(opposite_name => name)
|
||||
end
|
||||
|
||||
public :define_method
|
||||
|
||||
def define_methods (*names, &body)
|
||||
names.each { |name| define_method(name, &body) }
|
||||
end
|
||||
|
||||
def define_private_methods (*names, &body)
|
||||
define_methods(*names, &body)
|
||||
names.each { |name| private name }
|
||||
end
|
||||
|
||||
def define_protected_methods (*names, &body)
|
||||
define_methods(*names, &body)
|
||||
names.each { |name| protected name }
|
||||
end
|
||||
|
||||
def define_private_method (name, &body)
|
||||
define_method(name, &body)
|
||||
private name
|
||||
end
|
||||
|
||||
def define_protected_method (name, &body)
|
||||
define_method(name, &body)
|
||||
protected name
|
||||
end
|
||||
end
|
||||
|
||||
class ImmutableAttributeError < StandardError
|
||||
def initialize (attribute=nil, message=nil)
|
||||
super message
|
||||
@attribute = attribute
|
||||
end
|
||||
|
||||
define_accessors :attribute
|
||||
|
||||
def to_s
|
||||
if @attribute and @message
|
||||
"cannot change the value of `#@attribute': #@message"
|
||||
elsif @attribute
|
||||
"cannot change the value of `#@attribute'"
|
||||
elsif @message
|
||||
"cannot change the value of attribute: #@message"
|
||||
else
|
||||
"cannot change the value of attribute"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Module
|
||||
# Guard each of the specified attributes by replacing the writer
|
||||
# method with a proxy that asks the supplied block before proceeding
|
||||
# with the change.
|
||||
#
|
||||
# If it's okay to change the attribute, the block should return
|
||||
# either nil or the symbol :mutable. If it isn't okay, the block
|
||||
# should return a string saying why the attribute can't be changed.
|
||||
# If you don't want to provide a reason, you can have the block
|
||||
# return just the symbol :immutable.
|
||||
def guard_writers(*names, &predicate)
|
||||
for name in names.map { |x| x.to_sym } do
|
||||
define_hard_alias("__unguarded_#{name.writer}" => name.writer)
|
||||
define_method(name.writer) do |new_value|
|
||||
case result = predicate.call
|
||||
when :mutable, nil
|
||||
__send__("__unguarded_#{name.writer}", new_value)
|
||||
when :immutable
|
||||
raise ImmutableAttributeError.new(name)
|
||||
else
|
||||
raise ImmutableAttributeError.new(name, result)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def define_guarded_writers (*names, &block)
|
||||
define_writers(*names)
|
||||
guard_writers(*names, &block)
|
||||
end
|
||||
|
||||
define_soft_alias :guard_writer => :guard_writers
|
||||
define_soft_alias :define_guarded_writer => :define_guarded_writers
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
require "test/unit"
|
||||
|
||||
class DefineAccessorsTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@X = Class.new
|
||||
@Y = Class.new @X
|
||||
@x = @X.new
|
||||
@y = @Y.new
|
||||
end
|
||||
|
||||
def test_define_hard_aliases
|
||||
@X.define_method(:foo) { 123 }
|
||||
@X.define_method(:baz) { 321 }
|
||||
@X.define_hard_aliases :bar => :foo, :quux => :baz
|
||||
assert_equal @x.foo, 123
|
||||
assert_equal @x.bar, 123
|
||||
assert_equal @y.foo, 123
|
||||
assert_equal @y.bar, 123
|
||||
assert_equal @x.baz, 321
|
||||
assert_equal @x.quux, 321
|
||||
assert_equal @y.baz, 321
|
||||
assert_equal @y.quux, 321
|
||||
@Y.define_method(:foo) { 456 }
|
||||
assert_equal @y.foo, 456
|
||||
assert_equal @y.bar, 123
|
||||
@Y.define_method(:quux) { 654 }
|
||||
assert_equal @y.baz, 321
|
||||
assert_equal @y.quux, 654
|
||||
end
|
||||
|
||||
def test_define_soft_aliases
|
||||
@X.define_method(:foo) { 123 }
|
||||
@X.define_method(:baz) { 321 }
|
||||
@X.define_soft_aliases :bar => :foo, :quux => :baz
|
||||
assert_equal @x.foo, 123
|
||||
assert_equal @x.bar, 123
|
||||
assert_equal @y.foo, 123
|
||||
assert_equal @y.bar, 123
|
||||
assert_equal @x.baz, 321
|
||||
assert_equal @x.quux, 321
|
||||
assert_equal @y.baz, 321
|
||||
assert_equal @y.quux, 321
|
||||
@Y.define_method(:foo) { 456 }
|
||||
assert_equal @y.foo, @y.bar, 456
|
||||
@Y.define_method(:quux) { 654 }
|
||||
assert_equal @y.baz, 321
|
||||
assert_equal @y.quux, 654
|
||||
end
|
||||
|
||||
def test_define_readers
|
||||
@X.define_readers :foo, :bar
|
||||
assert !@x.respond_to?(:foo=)
|
||||
assert !@x.respond_to?(:bar=)
|
||||
@x.instance_eval { @foo = 123 ; @bar = 456 }
|
||||
assert_equal @x.foo, 123
|
||||
assert_equal @x.bar, 456
|
||||
@X.define_readers :baz?, :quux?
|
||||
assert !@x.respond_to?(:baz=)
|
||||
assert !@x.respond_to?(:quux=)
|
||||
@x.instance_eval { @baz = false ; @quux = true }
|
||||
assert !@x.baz?
|
||||
assert @x.quux?
|
||||
end
|
||||
|
||||
def test_define_writers
|
||||
assert !@X.writer_defined?(:foo)
|
||||
assert !@X.writer_defined?(:bar)
|
||||
@X.define_writers :foo, :bar
|
||||
assert @X.writer_defined?(:foo)
|
||||
assert @X.writer_defined?(:bar)
|
||||
assert @X.writer_defined?(:foo=)
|
||||
assert @X.writer_defined?(:bar=)
|
||||
assert @X.writer_defined?(:foo?)
|
||||
assert @X.writer_defined?(:bar?)
|
||||
assert !@x.respond_to?(:foo)
|
||||
assert !@x.respond_to?(:bar)
|
||||
@x.foo = 123
|
||||
@x.bar = 456
|
||||
assert_equal @x.instance_eval { @foo }, 123
|
||||
assert_equal @x.instance_eval { @bar }, 456
|
||||
@X.define_writers :baz?, :quux?
|
||||
assert !@x.respond_to?(:baz?)
|
||||
assert !@x.respond_to?(:quux?)
|
||||
@x.baz = true
|
||||
@x.quux = false
|
||||
assert_equal @x.instance_eval { @baz }, true
|
||||
assert_equal @x.instance_eval { @quux }, false
|
||||
end
|
||||
|
||||
def test_define_accessors
|
||||
@X.define_accessors :foo, :bar
|
||||
@x.foo = 123 ; @x.bar = 456
|
||||
assert_equal @x.foo, 123
|
||||
assert_equal @x.bar, 456
|
||||
end
|
||||
|
||||
def test_define_opposite_readers
|
||||
@X.define_opposite_readers :foo? => :bar?, :baz? => :quux?
|
||||
assert !@x.respond_to?(:foo=)
|
||||
assert !@x.respond_to?(:bar=)
|
||||
assert !@x.respond_to?(:baz=)
|
||||
assert !@x.respond_to?(:quux=)
|
||||
@x.instance_eval { @bar = true ; @quux = false }
|
||||
assert !@x.foo?
|
||||
assert @x.bar?
|
||||
assert @x.baz?
|
||||
assert !@x.quux?
|
||||
end
|
||||
|
||||
def test_define_opposite_writers
|
||||
@X.define_opposite_writers :foo? => :bar?, :baz => :quux
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,355 +0,0 @@
|
|||
## event-loop.rb --- high-level IO multiplexer
|
||||
# Copyright (C) 2005 Daniel Brockman
|
||||
|
||||
# This program is free software; you can redistribute it
|
||||
# and/or modify it under the terms of the GNU General Public
|
||||
# License as published by the Free Software Foundation;
|
||||
# either version 2 of the License, or (at your option) any
|
||||
# later version.
|
||||
|
||||
# This file is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public
|
||||
# License along with this program; if not, write to the Free
|
||||
# Software Foundation, 51 Franklin Street, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
require "puppet/external/event-loop/better-definers"
|
||||
require "puppet/external/event-loop/signal-system"
|
||||
|
||||
require "fcntl"
|
||||
|
||||
class EventLoop
|
||||
include SignalEmitter
|
||||
|
||||
IO_STATES = [:readable, :writable, :exceptional]
|
||||
|
||||
class << self
|
||||
def default ; @default ||= new end
|
||||
def default= x ; @default = x end
|
||||
|
||||
def current
|
||||
Thread.current["event-loop::current"] || default end
|
||||
def current= x
|
||||
Thread.current["event-loop::current"] = x end
|
||||
|
||||
def with_current (new)
|
||||
if current == new
|
||||
yield
|
||||
else
|
||||
begin
|
||||
old = self.current
|
||||
self.current = new
|
||||
yield
|
||||
ensure
|
||||
self.current = old
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def method_missing (name, *args, &block)
|
||||
if current.respond_to? name
|
||||
current.__send__(name, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
define_signals :before_sleep, :after_sleep
|
||||
|
||||
def initialize
|
||||
@running = false
|
||||
@awake = false
|
||||
@wakeup_time = nil
|
||||
@timers = []
|
||||
|
||||
@io_arrays = [[], [], []]
|
||||
@ios = Hash.new do |h, k| raise ArgumentError,
|
||||
"invalid IO event: #{k}", caller(2) end
|
||||
IO_STATES.each_with_index { |x, i| @ios[x] = @io_arrays[i] }
|
||||
|
||||
@notify_src, @notify_snk = IO.pipe
|
||||
|
||||
# prevent file descriptor leaks
|
||||
if @notify_src.respond_to?(:fcntl) and defined?(Fcntl) and defined?(Fcntl::F_SETFD) and defined?(Fcntl::FD_CLOEXEC)
|
||||
@notify_src.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
||||
@notify_snk.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
||||
end
|
||||
|
||||
@notify_src.will_block = false
|
||||
@notify_snk.will_block = false
|
||||
|
||||
# Each time a byte is sent through the notification pipe
|
||||
# we need to read it, or IO.select will keep returning.
|
||||
monitor_io(@notify_src, :readable)
|
||||
@notify_src.extend(Watchable)
|
||||
@notify_src.on_readable do
|
||||
begin
|
||||
@notify_src.sysread(256)
|
||||
rescue Errno::EAGAIN
|
||||
# The pipe wasn't readable after all.
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
define_opposite_accessors \
|
||||
:stopped? => :running?,
|
||||
:sleeping? => :awake?
|
||||
|
||||
def run
|
||||
if block_given?
|
||||
thread = Thread.new { run }
|
||||
yield ; quit ; thread.join
|
||||
else
|
||||
running!
|
||||
iterate while running?
|
||||
end
|
||||
ensure
|
||||
quit
|
||||
end
|
||||
|
||||
def iterate (user_timeout=nil)
|
||||
t1, t2 = user_timeout, max_timeout
|
||||
timeout = t1 && t2 ? [t1, t2].min : t1 || t2
|
||||
select(timeout).zip(IO_STATES) do |ios, state|
|
||||
ios.each { |x| x.signal(state) } if ios
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def select (timeout)
|
||||
@wakeup_time = timeout ? Time.now + timeout : nil
|
||||
# puts "waiting: #{timeout} seconds"
|
||||
signal :before_sleep ; sleeping!
|
||||
IO.select(*@io_arrays + [timeout]) || []
|
||||
ensure
|
||||
awake! ; signal :after_sleep
|
||||
@timers.each { |x| x.sound_alarm if x.ready? }
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
def quit ; stopped! ; wake_up ; self end
|
||||
|
||||
def monitoring_io? (io, event)
|
||||
@ios[event].include? io end
|
||||
def monitoring_timer? (timer)
|
||||
@timers.include? timer end
|
||||
|
||||
def monitor_io (io, *events)
|
||||
for event in events do
|
||||
@ios[event] << io ; wake_up unless monitoring_io?(io, event)
|
||||
end
|
||||
end
|
||||
|
||||
def monitor_timer (timer)
|
||||
@timers << timer unless monitoring_timer? timer
|
||||
end
|
||||
|
||||
def check_timer (timer)
|
||||
wake_up if timer.end_time < @wakeup_time
|
||||
end
|
||||
|
||||
def ignore_io (io, *events)
|
||||
events = IO_STATES if events.empty?
|
||||
for event in events do
|
||||
wake_up if @ios[event].delete(io)
|
||||
end
|
||||
end
|
||||
|
||||
def ignore_timer (timer)
|
||||
# Don't need to wake up for this.
|
||||
@timers.delete(timer)
|
||||
end
|
||||
|
||||
def max_timeout
|
||||
return nil if @timers.empty?
|
||||
[@timers.collect { |x| x.time_left }.min, 0].max
|
||||
end
|
||||
|
||||
def wake_up
|
||||
@notify_snk.write('.') if sleeping?
|
||||
end
|
||||
end
|
||||
|
||||
class Symbol
|
||||
def io_state?
|
||||
EventLoop::IO_STATES.include? self
|
||||
end
|
||||
end
|
||||
|
||||
module EventLoop::Watchable
|
||||
include SignalEmitter
|
||||
|
||||
define_signals :readable, :writable, :exceptional
|
||||
|
||||
def monitor_events (*events)
|
||||
EventLoop.monitor_io(self, *events) end
|
||||
def ignore_events (*events)
|
||||
EventLoop.ignore_io(self, *events) end
|
||||
|
||||
define_soft_aliases \
|
||||
:monitor_event => :monitor_events,
|
||||
:ignore_event => :ignore_events
|
||||
|
||||
def close ; super
|
||||
ignore_events end
|
||||
def close_read ; super
|
||||
ignore_event :readable end
|
||||
def close_write ; super
|
||||
ignore_event :writable end
|
||||
|
||||
module Automatic
|
||||
include EventLoop::Watchable
|
||||
|
||||
def add_signal_handler (name, &handler) super
|
||||
monitor_event(name) if name.io_state?
|
||||
end
|
||||
|
||||
def remove_signal_handler (name, handler) super
|
||||
if @signal_handlers[name].empty?
|
||||
ignore_event(name) if name.io_state?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class IO
|
||||
def on_readable &block
|
||||
extend EventLoop::Watchable::Automatic
|
||||
on_readable(&block)
|
||||
end
|
||||
|
||||
def on_writable &block
|
||||
extend EventLoop::Watchable::Automatic
|
||||
on_writable(&block)
|
||||
end
|
||||
|
||||
def on_exceptional &block
|
||||
extend EventLoop::Watchable::Automatic
|
||||
on_exceptional(&block)
|
||||
end
|
||||
|
||||
def will_block?
|
||||
if respond_to?(:fcntl) and defined?(Fcntl) and defined?(Fcntl::F_GETFL) and defined?(Fcntl::O_NONBLOCK)
|
||||
fcntl(Fcntl::F_GETFL, 0) & Fcntl::O_NONBLOCK == 0
|
||||
end
|
||||
end
|
||||
|
||||
def will_block= (wants_blocking)
|
||||
if respond_to?(:fcntl) and defined?(Fcntl) and defined?(Fcntl::F_GETFL) and defined?(Fcntl::O_NONBLOCK)
|
||||
flags = fcntl(Fcntl::F_GETFL, 0)
|
||||
if wants_blocking
|
||||
flags &= ~Fcntl::O_NONBLOCK
|
||||
else
|
||||
flags |= Fcntl::O_NONBLOCK
|
||||
end
|
||||
fcntl(Fcntl::F_SETFL, flags)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class EventLoop::Timer
|
||||
include SignalEmitter
|
||||
|
||||
DEFAULT_INTERVAL = 0.0
|
||||
DEFAULT_TOLERANCE = 0.001
|
||||
|
||||
def initialize (options={}, &handler)
|
||||
@running = false
|
||||
@start_time = nil
|
||||
|
||||
options = { :interval => options } if options.kind_of? Numeric
|
||||
|
||||
if options[:interval]
|
||||
@interval = options[:interval].to_f
|
||||
else
|
||||
@interval = DEFAULT_INTERVAL
|
||||
end
|
||||
|
||||
if options[:tolerance]
|
||||
@tolerance = options[:tolerance].to_f
|
||||
elsif DEFAULT_TOLERANCE < @interval
|
||||
@tolerance = DEFAULT_TOLERANCE
|
||||
else
|
||||
@tolerance = 0.0
|
||||
end
|
||||
|
||||
@event_loop = options[:event_loop] || EventLoop.current
|
||||
|
||||
if block_given?
|
||||
add_signal_handler(:alarm, &handler)
|
||||
start unless options[:start?] == false
|
||||
else
|
||||
start if options[:start?]
|
||||
end
|
||||
end
|
||||
|
||||
define_readers :interval, :tolerance
|
||||
define_signal :alarm
|
||||
|
||||
def stopped? ; @start_time == nil end
|
||||
def running? ; @start_time != nil end
|
||||
|
||||
def interval= (new_interval)
|
||||
old_interval = @interval
|
||||
@interval = new_interval
|
||||
@event_loop.check_timer(self) if new_interval < old_interval
|
||||
end
|
||||
|
||||
def end_time
|
||||
@start_time + @interval end
|
||||
def time_left
|
||||
end_time - Time.now end
|
||||
def ready?
|
||||
time_left <= @tolerance end
|
||||
|
||||
def restart
|
||||
@start_time = Time.now
|
||||
end
|
||||
|
||||
def sound_alarm
|
||||
signal :alarm
|
||||
restart if running?
|
||||
end
|
||||
|
||||
def start
|
||||
@start_time = Time.now
|
||||
@event_loop.monitor_timer(self)
|
||||
end
|
||||
|
||||
def stop
|
||||
@start_time = nil
|
||||
@event_loop.ignore_timer(self)
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
require "test/unit"
|
||||
|
||||
class TimerTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@timer = EventLoop::Timer.new(:interval => 0.001)
|
||||
end
|
||||
|
||||
def test_timer
|
||||
@timer.on_alarm do
|
||||
puts "[#{@timer.time_left} seconds left after alarm]"
|
||||
EventLoop.quit
|
||||
end
|
||||
8.times do
|
||||
t0 = Time.now
|
||||
@timer.start ; EventLoop.run
|
||||
t1 = Time.now
|
||||
assert(t1 - t0 > @timer.interval - @timer.tolerance)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## event-loop.rb ends here.
|
|
@ -1,218 +0,0 @@
|
|||
## signal-system.rb --- simple intra-process signal system
|
||||
# Copyright (C) 2005 Daniel Brockman
|
||||
|
||||
# This program is free software; you can redistribute it
|
||||
# and/or modify it under the terms of the GNU General Public
|
||||
# License as published by the Free Software Foundation;
|
||||
# either version 2 of the License, or (at your option) any
|
||||
# later version.
|
||||
|
||||
# This file is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public
|
||||
# License along with this program; if not, write to the Free
|
||||
# Software Foundation, 51 Franklin Street, Fifth Floor,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
require "puppet/external/event-loop/better-definers"
|
||||
|
||||
module SignalEmitterModule
|
||||
def self.extended (object)
|
||||
if object.kind_of? Module and not object < SignalEmitter
|
||||
if object.respond_to? :fcall
|
||||
# This is the way to call private methods
|
||||
# in Ruby 1.9 as of November 16.
|
||||
object.fcall :include, SignalEmitter
|
||||
else
|
||||
object.__send__ :include, SignalEmitter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def define_signal (name, slot=:before, &body)
|
||||
# Can't use `define_method' and take a block pre-1.9.
|
||||
class_eval %{ def on_#{name} &block
|
||||
add_signal_handler(:#{name}, &block) end }
|
||||
define_signal_handler(name, :before, &lambda {|*a|})
|
||||
define_signal_handler(name, :after, &lambda {|*a|})
|
||||
define_signal_handler(name, slot, &body) if block_given?
|
||||
end
|
||||
|
||||
def define_signals (*names, &body)
|
||||
names.each { |x| define_signal(x, &body) }
|
||||
end
|
||||
|
||||
def define_signal_handler (name, slot=:before, &body)
|
||||
case slot
|
||||
when :before
|
||||
define_protected_method "handle_#{name}", &body
|
||||
when :after
|
||||
define_protected_method "after_handle_#{name}", &body
|
||||
else
|
||||
raise ArgumentError, "invalid slot `#{slot.inspect}'; " +
|
||||
"should be `:before' or `:after'", caller(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This is an old name for the same thing.
|
||||
SignalEmitterClass = SignalEmitterModule
|
||||
|
||||
module SignalEmitter
|
||||
def self.included (includer)
|
||||
includer.extend SignalEmitterClass if not includer.kind_of? SignalEmitterClass
|
||||
end
|
||||
|
||||
def __maybe_initialize_signal_emitter
|
||||
@signal_handlers ||= Hash.new { |h, k| h[k] = Array.new }
|
||||
@allow_dynamic_signals ||= false
|
||||
end
|
||||
|
||||
define_accessors :allow_dynamic_signals?
|
||||
|
||||
def add_signal_handler (name, &handler)
|
||||
__maybe_initialize_signal_emitter
|
||||
@signal_handlers[name] << handler
|
||||
handler
|
||||
end
|
||||
|
||||
define_soft_aliases [:on, :on_signal] => :add_signal_handler
|
||||
|
||||
def remove_signal_handler (name, handler)
|
||||
__maybe_initialize_signal_emitter
|
||||
@signal_handlers[name].delete(handler)
|
||||
end
|
||||
|
||||
def __signal__ (name, *args, &block)
|
||||
__maybe_initialize_signal_emitter
|
||||
respond_to? "on_#{name}" or allow_dynamic_signals? or
|
||||
fail "undefined signal `#{name}' for #{self}:#{self.class}"
|
||||
__send__("handle_#{name}", *args, &block) if
|
||||
respond_to? "handle_#{name}"
|
||||
@signal_handlers[name].each { |x| x.call(*args, &block) }
|
||||
__send__("after_handle_#{name}", *args, &block) if
|
||||
respond_to? "after_handle_#{name}"
|
||||
end
|
||||
|
||||
define_soft_alias :signal => :__signal__
|
||||
end
|
||||
|
||||
# This module is indended to be a convenience mixin to be used by
|
||||
# classes whose objects need to observe foreign signals. That is,
|
||||
# if you want to observe some signals coming from an object, *you*
|
||||
# should mix in this module.
|
||||
#
|
||||
# You cannot use this module at two different places of the same
|
||||
# inheritance chain to observe signals coming from the same object.
|
||||
#
|
||||
# XXX: This has not seen much use, and I'd like to provide a
|
||||
# better solution for the problem in the future.
|
||||
module SignalObserver
|
||||
def __maybe_initialize_signal_observer
|
||||
@observed_signals ||= Hash.new do |signals, object|
|
||||
signals[object] = Hash.new do |handlers, name|
|
||||
handlers[name] = Array.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def observe_signal (subject, name, &handler)
|
||||
__maybe_initialize_signal_observer
|
||||
@observed_signals[subject][name] << handler
|
||||
subject.add_signal_handler(name, &handler)
|
||||
end
|
||||
|
||||
def map_signals (source, pairs={})
|
||||
pairs.each do |src_name, dst_name|
|
||||
observe_signal(source, src_name) do |*args|
|
||||
__signal__(dst_name, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def absorb_signals (subject, *names)
|
||||
names.each do |name|
|
||||
observe_signal(subject, name) do |*args|
|
||||
__signal__(name, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
define_soft_aliases \
|
||||
:map_signal => :map_signals,
|
||||
:absorb_signal => :absorb_signals
|
||||
|
||||
def ignore_signal (subject, name)
|
||||
__maybe_initialize_signal_observer
|
||||
__ignore_signal_1(subject, name)
|
||||
@observed_signals.delete(subject) if
|
||||
@observed_signals[subject].empty?
|
||||
end
|
||||
|
||||
def ignore_signals (subject, *names)
|
||||
__maybe_initialize_signal_observer
|
||||
names = @observed_signals[subject] if names.empty?
|
||||
names.each { |x| __ignore_signal_1(subject, x) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def __ignore_signal_1(subject, name)
|
||||
@observed_signals[subject][name].each do |handler|
|
||||
subject.remove_signal_handler(name, handler) end
|
||||
@observed_signals[subject].delete(name)
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
require "test/unit"
|
||||
class SignalEmitterTest < Test::Unit::TestCase
|
||||
class X
|
||||
include SignalEmitter
|
||||
define_signal :foo
|
||||
end
|
||||
|
||||
def setup
|
||||
@x = X.new
|
||||
end
|
||||
|
||||
def test_on_signal
|
||||
moomin = 0
|
||||
@x.on_signal(:foo) { moomin = 1 }
|
||||
@x.signal :foo
|
||||
assert moomin == 1
|
||||
end
|
||||
|
||||
def test_on_foo
|
||||
moomin = 0
|
||||
@x.on_foo { moomin = 1 }
|
||||
@x.signal :foo
|
||||
assert moomin == 1
|
||||
end
|
||||
|
||||
def test_multiple_on_signal
|
||||
moomin = 0
|
||||
@x.on_signal(:foo) { moomin += 1 }
|
||||
@x.on_signal(:foo) { moomin += 2 }
|
||||
@x.on_signal(:foo) { moomin += 4 }
|
||||
@x.on_signal(:foo) { moomin += 8 }
|
||||
@x.signal :foo
|
||||
assert moomin == 15
|
||||
end
|
||||
|
||||
def test_multiple_on_foo
|
||||
moomin = 0
|
||||
@x.on_foo { moomin += 1 }
|
||||
@x.on_foo { moomin += 2 }
|
||||
@x.on_foo { moomin += 4 }
|
||||
@x.on_foo { moomin += 8 }
|
||||
@x.signal :foo
|
||||
assert moomin == 15
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## application-signals.rb ends here.
|
|
@ -212,7 +212,7 @@ WRAPPER
|
|||
|
||||
if @face.is_a?(Class)
|
||||
@face.class_eval do eval wrapper, nil, file, line end
|
||||
@face.define_method(internal_name, &block)
|
||||
@face.send(:define_method, internal_name, &block)
|
||||
@when_invoked = @face.instance_method(name)
|
||||
else
|
||||
@face.instance_eval do eval wrapper, nil, file, line end
|
||||
|
|
|
@ -10,13 +10,13 @@ module Puppet::Module::Tool
|
|||
# Read the +filename+ and eval its Ruby code to set values in the Metadata
|
||||
# +metadata+ instance.
|
||||
def self.evaluate(metadata, filename)
|
||||
returning(new(metadata)) do |builder|
|
||||
if File.file?(filename)
|
||||
builder.instance_eval(File.read(filename.to_s), filename.to_s, 1)
|
||||
else
|
||||
Puppet.warning "No Modulefile: #{filename}"
|
||||
end
|
||||
builder = new(metadata)
|
||||
if File.file?(filename)
|
||||
builder.instance_eval(File.read(filename.to_s), filename.to_s, 1)
|
||||
else
|
||||
Puppet.warning "No Modulefile: #{filename}"
|
||||
end
|
||||
return builder
|
||||
end
|
||||
|
||||
# Instantiate with the Metadata +metadata+ instance.
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
require 'puppet'
|
||||
require 'sync'
|
||||
require 'getoptlong'
|
||||
require 'puppet/external/event-loop'
|
||||
require 'puppet/util/loadedfile'
|
||||
|
||||
# The class for handling configuration files.
|
||||
|
@ -296,10 +295,6 @@ class Puppet::Util::Settings
|
|||
@sync.synchronize do
|
||||
unsafe_parse(self[:config])
|
||||
end
|
||||
|
||||
# Create a timer so that this file will get checked automatically
|
||||
# and reparsed if necessary.
|
||||
set_filetimeout_timer
|
||||
end
|
||||
|
||||
# Unsafely parse the file -- this isn't thread-safe and causes plenty of problems if used directly.
|
||||
|
@ -549,12 +544,6 @@ class Puppet::Util::Settings
|
|||
call.each { |setting| setting.handle(self.value(setting.name)) }
|
||||
end
|
||||
|
||||
# Create a timer to check whether the file should be reparsed.
|
||||
def set_filetimeout_timer
|
||||
return unless timeout = self[:filetimeout] and timeout = Integer(timeout) and timeout > 0
|
||||
timer = EventLoop::Timer.new(:interval => timeout, :tolerance => 1, :start? => true) { self.reparse }
|
||||
end
|
||||
|
||||
# Convert the settings we manage into a catalog full of resources that model those settings.
|
||||
def to_catalog(*sections)
|
||||
sections = nil if sections.empty?
|
||||
|
|
|
@ -270,36 +270,4 @@ describe Puppet::Agent do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "when starting" do
|
||||
before do
|
||||
@agent.stubs(:observe_signal)
|
||||
end
|
||||
|
||||
it "should create a timer with the runinterval, a tolerance of 1, and :start? set to true" do
|
||||
Puppet.settings.expects(:value).with(:runinterval).returns 5
|
||||
timer = stub 'timer', :sound_alarm => nil
|
||||
EventLoop::Timer.expects(:new).with(:interval => 5, :start? => true, :tolerance => 1).returns timer
|
||||
|
||||
@agent.stubs(:run)
|
||||
@agent.start
|
||||
end
|
||||
|
||||
it "should run once immediately" do
|
||||
timer = mock 'timer'
|
||||
EventLoop::Timer.expects(:new).returns timer
|
||||
|
||||
timer.expects(:sound_alarm)
|
||||
|
||||
@agent.start
|
||||
end
|
||||
|
||||
it "should run within the block passed to the timer" do
|
||||
timer = stub 'timer', :sound_alarm => nil
|
||||
EventLoop::Timer.expects(:new).returns(timer).yields
|
||||
@agent.expects(:run)
|
||||
|
||||
@agent.start
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,7 +49,7 @@ describe Puppet::Daemon do
|
|||
before do
|
||||
@daemon.stubs(:create_pidfile)
|
||||
@daemon.stubs(:set_signal_traps)
|
||||
EventLoop.current.stubs(:run)
|
||||
@daemon.stubs(:run_event_loop)
|
||||
end
|
||||
|
||||
it "should fail if it has neither agent nor server" do
|
||||
|
@ -58,19 +58,10 @@ describe Puppet::Daemon do
|
|||
|
||||
it "should create its pidfile" do
|
||||
@daemon.stubs(:agent).returns stub('agent', :start => nil)
|
||||
|
||||
@daemon.expects(:create_pidfile)
|
||||
@daemon.start
|
||||
end
|
||||
|
||||
it "should start the agent if the agent is configured" do
|
||||
agent = mock 'agent'
|
||||
agent.expects(:start)
|
||||
@daemon.stubs(:agent).returns agent
|
||||
|
||||
@daemon.start
|
||||
end
|
||||
|
||||
it "should start its server if one is configured" do
|
||||
server = mock 'server'
|
||||
server.expects(:start)
|
||||
|
@ -78,13 +69,6 @@ describe Puppet::Daemon do
|
|||
|
||||
@daemon.start
|
||||
end
|
||||
|
||||
it "should let the current EventLoop run" do
|
||||
@daemon.stubs(:agent).returns stub('agent', :start => nil)
|
||||
EventLoop.current.expects(:run)
|
||||
|
||||
@daemon.start
|
||||
end
|
||||
end
|
||||
|
||||
describe "when stopping" do
|
||||
|
|
|
@ -411,13 +411,6 @@ describe Puppet::Util::Settings do
|
|||
@settings.parse
|
||||
end
|
||||
|
||||
it "should set a timer that triggers reparsing, even if the file does not exist" do
|
||||
FileTest.expects(:exist?).returns false
|
||||
@settings.expects(:set_filetimeout_timer)
|
||||
|
||||
@settings.parse
|
||||
end
|
||||
|
||||
it "should return values set in the configuration file" do
|
||||
text = "[main]
|
||||
one = fileval
|
||||
|
@ -559,12 +552,6 @@ describe Puppet::Util::Settings do
|
|||
File.expects(:expand_path).with(somefile).returns somefile
|
||||
@settings[:config] = somefile
|
||||
end
|
||||
|
||||
it "should not set a timer" do
|
||||
EventLoop::Timer.expects(:new).never
|
||||
|
||||
@settings.parse
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1057,46 +1044,6 @@ describe Puppet::Util::Settings do
|
|||
end
|
||||
end
|
||||
|
||||
describe "when setting a timer to trigger configuration file reparsing" do
|
||||
before do
|
||||
@settings = Puppet::Util::Settings.new
|
||||
@settings.setdefaults :foo, :filetimeout => [5, "eh"]
|
||||
end
|
||||
|
||||
it "should do nothing if no filetimeout setting is available" do
|
||||
@settings.expects(:value).with(:filetimeout).returns nil
|
||||
EventLoop::Timer.expects(:new).never
|
||||
@settings.set_filetimeout_timer
|
||||
end
|
||||
|
||||
it "should always convert the timer interval to an integer" do
|
||||
@settings.expects(:value).with(:filetimeout).returns "10"
|
||||
EventLoop::Timer.expects(:new).with(:interval => 10, :start? => true, :tolerance => 1)
|
||||
@settings.set_filetimeout_timer
|
||||
end
|
||||
|
||||
it "should do nothing if the filetimeout setting is not greater than 0" do
|
||||
@settings.expects(:value).with(:filetimeout).returns -2
|
||||
EventLoop::Timer.expects(:new).never
|
||||
@settings.set_filetimeout_timer
|
||||
end
|
||||
|
||||
it "should create a timer with its interval set to the filetimeout, start? set to true, and a tolerance of 1" do
|
||||
@settings.expects(:value).with(:filetimeout).returns 5
|
||||
EventLoop::Timer.expects(:new).with(:interval => 5, :start? => true, :tolerance => 1)
|
||||
|
||||
@settings.set_filetimeout_timer
|
||||
end
|
||||
|
||||
it "should reparse when the timer goes off" do
|
||||
EventLoop::Timer.expects(:new).with(:interval => 5, :start? => true, :tolerance => 1).yields
|
||||
|
||||
@settings.expects(:reparse)
|
||||
|
||||
@settings.set_filetimeout_timer
|
||||
end
|
||||
end
|
||||
|
||||
describe "when determining if the service user is available" do
|
||||
it "should return false if there is no user setting" do
|
||||
Puppet::Util::Settings.new.should_not be_service_user_available
|
||||
|
|
|
@ -8,7 +8,6 @@ require 'puppettest'
|
|||
# Test the different features of the main puppet module
|
||||
class TestPuppetModule < Test::Unit::TestCase
|
||||
include PuppetTest
|
||||
include SignalObserver
|
||||
|
||||
def mkfakeclient
|
||||
Class.new(Puppet::Network::Client) do
|
||||
|
|
Загрузка…
Ссылка в новой задаче