diff options
author | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-09-16 12:47:52 +0100 |
---|---|---|
committer | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-09-16 12:47:52 +0100 |
commit | 38e4d877abee3c8e40edd932057e2bf16ad01e13 (patch) | |
tree | d2201a1c18fbebec4e0594b81c27974057e886b2 | |
parent | f63d7076e52a8844f1cfe43e57330687d88e83b6 (diff) |
Big documentation update.
36 files changed, 1669 insertions, 522 deletions
diff --git a/lib/mauve/alert.rb b/lib/mauve/alert.rb index fa2ce65..25cee54 100644 --- a/lib/mauve/alert.rb +++ b/lib/mauve/alert.rb @@ -6,6 +6,10 @@ require 'mauve/source_list' require 'sanitize' module Mauve + # + # This is a view of the Alert table, which allows easy finding of the next + # alert due to trigger. + # class AlertEarliestDate include DataMapper::Resource @@ -52,9 +56,14 @@ module Mauve end end - + + # + # Woo! An alert. + # class Alert + # @deprecated Not used anymore? def bytesize; 99; end + # @deprecated Not used anymore? def size; 99; end include DataMapper::Resource @@ -89,31 +98,23 @@ module Mauve validates_with_method :check_dates + default_scope(:default).update(:order => [:source, :importance]) + + # @return [String] def to_s "#<Alert #{id}, alert_id #{alert_id}, source #{source}>" end - # - # This is to stop datamapper inserting duff dates into the database. - # - def check_dates - bad_dates = self.attributes.find_all do |key, value| - value.is_a?(Time) and (value < (Time.now - 3650.days) or value > (Time.now + 3650.days)) - end - - if bad_dates.empty? - true - else - [ false, "The dates "+bad_dates.collect{|k,v| "#{v.to_s} (#{k})"}.join(", ")+" are invalid." ] - end - end - - default_scope(:default).update(:order => [:source, :importance]) - + # @return [Log4r::Logger] the logger instance. def logger @logger ||= self.class.logger end + # @deprecated Not sure if this is used any more. + # + # @param [Integer] Seconds + # + # @return [String] def time_relative(secs) secs = secs.to_i.abs case secs @@ -127,45 +128,63 @@ module Mauve end # - # AlertGroup.matches must always return a an array of groups. - # + # @return [Mauve::AlertGroup] The first matching AlertGroup for this alert def alert_group @alert_group ||= AlertGroup.matches(self).first end - # # Pick out the source lists that match this alert by subject. # + # @return [Array] All the SourceList matches def source_lists Mauve::Configuration.current.source_lists.select{|label, list| list.includes?(self.subject)}.collect{|sl| sl.first} end - def in_source_list?(g) - list = Mauve::Configuration.current.source_lists[g] + # Checks to see if included in a named source list + # + # @param [String] listname + # @return [Boolean] + def in_source_list?(listname) + list = Mauve::Configuration.current.source_lists[listname] + return false unless list.is_a?(SourceList) list.includes?(self.subject) end + # Returns the alert level # - # - # + # @return [Symbol] The alert level, as per its AlertGroup. def level @level ||= self.alert_group.level end + # An array used to sort compare + # + # @return [Array] def sort_tuple [AlertGroup::LEVELS.index(self.level), (self.raised_at || self.cleared_at || Time.now)] end + # Comparator. Uses sort_tuple to compare with another alert + # + # @param [Mauve::Alert] other Other alert + # + # @return [Integer] def <=>(other) other.sort_tuple <=> self.sort_tuple end + # The alert subject + # + # @return [String] def subject; attribute_get(:subject) || attribute_get(:source) || "not set" ; end + + # The alert detail + # + # @return [String] def detail; attribute_get(:detail) || "_No detail set._" ; end protected - # # This cleans the HTML before saving. # def do_sanitize_html @@ -185,8 +204,21 @@ module Mauve attribute_set(key, Alert.clean_html(val)) end end - + + # This is to stop datamapper inserting duff dates into the database. # + def check_dates + bad_dates = self.attributes.find_all do |key, value| + value.is_a?(Time) and (value < (Time.now - 3650.days) or value > (Time.now + 3650.days)) + end + + if bad_dates.empty? + true + else + [ false, "The dates "+bad_dates.collect{|k,v| "#{v.to_s} (#{k})"}.join(", ")+" are invalid." ] + end + end + # This allows us to take a copy of the changes before we save. # def take_copy_of_changes @@ -196,9 +228,9 @@ module Mauve end end - # # This sends notifications. It is called after each save. # + # @return [Boolean] def notify_if_needed # # Make sure we don't barf @@ -243,20 +275,33 @@ module Mauve true end + # Remove all history for an alert, when an alert is destroyed. + # + # def destroy_associations AlertHistory.all(:alert_id => self.id).destroy end public + # Send a notification for this alert. + # + # @return [Boolean] Showing if an alert has been sent. def notify if self.alert_group.nil? logger.warn "Could not notify for #{self} since there are no matching alert groups" + false else self.alert_group.notify(self) end end + # Acknowledge an alert + # + # @param [Mauve::Person] person The person acknowledging the alert + # @param [Time] ack_until The time when the alert should unacknowledge + # + # @return [Boolean] showing the acknowledgment has been successful def acknowledge!(person, ack_until = Time.now+3600) raise ArgumentError unless person.is_a?(Person) raise ArgumentError unless ack_until.is_a?(Time) @@ -281,6 +326,9 @@ module Mauve end end + # Unacknowledge an alert + # + # @return [Boolean] showing the unacknowledgment has been successful def unacknowledge! self.acknowledged_by = nil self.acknowledged_at = nil @@ -294,7 +342,12 @@ module Mauve true end end - + + # Raise an alert at a specified time + # + # @param [Time] at The time at which the alert should be raised. + # + # @return [Boolean] showing the raise has been successful def raise!(at = Time.now) # # OK if this is an alert updated in the last run, do not raise, just postpone. @@ -334,6 +387,11 @@ module Mauve end end + # Clear an alert at a specified time + # + # @param [Time] at The time at which the alert should be cleared. + # + # @return [Boolean] showing the clear has been successful def clear!(at = Time.now) # # Postpone clearance if we're in the sleep period. @@ -379,47 +437,57 @@ module Mauve end end - # Returns the time at which a timer loop should call poll_event to either - # raise, clear or unacknowldge this event. - # + # The next time this alert should be polled, either to raise, clear, or + # unacknowledge, or nil if nothing is due. + # + # @return [Time, NilClass] def due_at [will_clear_at, will_raise_at, will_unacknowledge_at].compact.sort.first end + # Polls the alert, raising or clearing as needed. + # + # @return [Boolean] showing the poll was successful def poll logger.debug("Polling #{self.to_s}") - raise! if (will_unacknowledge_at and will_unacknowledge_at <= Time.now) or + if (will_unacknowledge_at and will_unacknowledge_at <= Time.now) or (will_raise_at and will_raise_at <= Time.now) - clear! if will_clear_at && will_clear_at <= Time.now + raise! + elsif will_clear_at && will_clear_at <= Time.now + clear! + else + true + end end + # Is the alert raised? # - # Tests to see if an alert is raised/acknowledged given a certain set of - # dates/times. - # - # - + # @return [Boolean] def raised? !raised_at.nil? and (cleared_at.nil? or raised_at > cleared_at) end + # Is the alert acknowledged? + # + # @return [Boolean] def acknowledged? !acknowledged_at.nil? end + # Is the alert cleared? Cleared is just the opposite of raised. # - # Cleared is just the opposite of raised. - # + # @return [Boolean] def cleared? !raised? end class << self + # Removes HTML from a string # - # Utility methods to clean/remove html - # + # @param [String] txt String to clean + # @return [String] def remove_html(txt) Sanitize.clean( txt.to_s, @@ -427,6 +495,10 @@ module Mauve ) end + # Cleans HTML in a string, removing dangerous elements/contents. + # + # @param [String] txt String to clean + # @return [String] def clean_html(txt) Sanitize.clean( txt.to_s, @@ -434,22 +506,30 @@ module Mauve ) end + # All alerts currently raised # - # Find stuff - # - # + # @return [Array] def all_raised all(:raised_at.not => nil, :order => [:raised_at.asc]) & (all(:cleared_at => nil) | all(:raised_at.gte => :cleared_at)) end + # All alerts currently raised and unacknowledged + # + # @return [Array] def all_unacknowledged all_raised - all_acknowledged end + # All alerts currently acknowledged + # + # @return [Array] def all_acknowledged all(:acknowledged_at.not => nil) end + # All alerts currently cleared + # + # @return [Array] def all_cleared all - all_raised - all_acknowledged end @@ -469,24 +549,31 @@ module Mauve return hash end - # - # Returns the next Alert that will have a timed action due on it, or nil - # if none are pending. + # Find the next Alert that will have a timed action due on it, or nil if + # none are pending. # + # @return [Mauve::Alert, Nilclass] def find_next_with_event earliest_alert = AlertEarliestDate.first(:order => [:earliest]) earliest_alert ? earliest_alert.alert : nil end + # @deprecated Not sure this is used any more. + # + # @return [Array] def all_overdue(at = Time.now) AlertEarliestDate.all(:earliest.lt => at, :order => [:earliest]).collect do |earliest_alert| earliest_alert ? earliest_alert.alert : nil end end - # # Receive an AlertUpdate buffer from the wire. # + # @param [String] update The update string, as received over UDP + # @param [Time] reception_time The time the update was received + # @param [String] ip_source The IP address of the source of the update + # + # @return [NilClass] def receive_update(update, reception_time = Time.now, ip_source="network") update = Proto::AlertUpdate.parse_from_string(update) unless update.kind_of?(Proto::AlertUpdate) @@ -631,6 +718,8 @@ module Mauve return nil end + # + # @return [Log4r::Logger] The class logger def logger Log4r::Logger.new(self.to_s) end diff --git a/lib/mauve/alert_changed.rb b/lib/mauve/alert_changed.rb index ac864a0..c36c4c8 100644 --- a/lib/mauve/alert_changed.rb +++ b/lib/mauve/alert_changed.rb @@ -3,6 +3,9 @@ require 'mauve/datamapper' require 'log4r' module Mauve + # + # Class to record changes to alerts. Also responsible for keeping records for reminders. + # class AlertChanged include DataMapper::Resource @@ -17,26 +20,37 @@ module Mauve property :level, String, :required => true property :update_type, String, :required => true property :remind_at, Time - # property :updated_at, Time, :required => true + + belongs_to :alert + + # @return [String] def to_s "#<AlertChanged #{id}: alert_id #{alert_id}, for #{person}, update_type #{update_type}>" end - belongs_to :alert - + # @deprecated I don't think was_relevant is used any more. + # def was_relevant=(value) attribute_set(:was_relevant, value) end + # The time this object was last updated + # + # @return [Time] def updated_at self.at end + # Set the time this object was last updated + # + # @param [Time] t + # @return [Time] def updated_at=(t) self.at = t end + # @return [Log4r::Logger] def logger Log4r::Logger.new self.class.to_s end @@ -44,6 +58,7 @@ module Mauve # Sends a reminder about this alert state change, or forget about it if # the alert has been acknowledged # + # @return [Boolean] indicating successful update of the AlertChanged object def remind unless alert.is_a?(Alert) logger.info "#{self.inspect} lost alert #{alert_id}. Killing self." @@ -83,28 +98,52 @@ module Mauve save end - def due_at # mimic interface from Alert + # The time this AlertChanged should next be polled at, or nil. Mimics + # interaface from Alert. + # + # @return [Time, NilClass] + def due_at remind_at ? remind_at : nil end + # Sends a reminder, if needed. Mimics interaface from Alert. + # + # @return [Boolean] showing polling was successful def poll # mimic interface from Alert logger.debug("Polling #{self.to_s}") - remind if remind_at.is_a?(Time) and remind_at <= Time.now + + if remind_at.is_a?(Time) and remind_at <= Time.now + remind + else + true + end end + # The AlertGroup for this object + # + # @return [Mauve::AlertGroup] def alert_group alert.alert_group end class << self + # Finds the next reminder due, or nil if nothing due. + # + # @return [Mauve::AlertChanged, NilClass] def next_reminder first(:remind_at.not => nil, :order => [:remind_at]) end - def find_next_with_event # mimic interface from Alert + # Finds the next event due. Mimics interface from Alert. + # + # @return [Mauve::AlertChanged, NilClass] + def find_next_with_event next_reminder end + # @deprecated I don't think this is used any more. + # + # @return [Array] def all_overdue(at = Time.now) all(:remind_at.not => nil, :remind_at.lt => at, :order => [:remind_at]).to_a end diff --git a/lib/mauve/alert_group.rb b/lib/mauve/alert_group.rb index b320f18..7ea7ffb 100644 --- a/lib/mauve/alert_group.rb +++ b/lib/mauve/alert_group.rb @@ -3,6 +3,11 @@ require 'mauve/alert' require 'log4r' module Mauve + # + # This corresponds to a alert_group clause in the configuration. It is what + # is used to classify alerts into levels, and thus who gets notified about it + # and when. + # class AlertGroup < Struct.new(:name, :includes, :acknowledgement_time, :level, :notifications) # @@ -15,6 +20,11 @@ module Mauve class << self + # Finds all AlertGroups that match an alert. + # + # @param [Mauve::Alert] alert + # + # @return [Array] AlertGroups that match def matches(alert) grps = all.select { |alert_group| alert_group.includes?(alert) } @@ -26,10 +36,14 @@ module Mauve grps end + # @return [Log4r::Logger] def logger Log4r::Logger.new self.to_s end + # All AlertGroups + # + # @return [Array] def all return [] if Configuration.current.nil? @@ -44,6 +58,7 @@ module Mauve # the first one should be returned thus making this useless. If you want # a list of all the alerts matching a level, use Alert.get_all(). # + # @return [Array] def all_alerts_by_level(level) Configuration.current.alert_groups.map do |alert_group| alert_group.level == level ? alert_group.current_alerts : [] @@ -52,18 +67,26 @@ module Mauve end + # Creates a new AlertGroup + # + # @param name Name of alert group + # + # @return [AlertGroup] self def initialize(name) self.name = name self.level = :normal self.includes = Proc.new { true } + self end - def inspect + # @return [String] + def to_s "#<AlertGroup:#{name} (level #{level})>" end # The list of current raised alerts in this group. # + # @return [Array] Array of Mauve::Alert def current_alerts Alert.all(:cleared_at => nil, :raised_at.not => nil).select { |a| includes?(a) } end @@ -71,7 +94,8 @@ module Mauve # Decides whether a given alert belongs in this group according to its # includes { } clause # - # @param [Alert] alert An alert to test for belongness to group. + # @param [Mauve::Alert] alert + # # @return [Boolean] Success or failure. def includes?(alert) @@ -86,18 +110,22 @@ module Mauve alias matches_alert? includes? + # @return [Log4r::Logger] def logger ; self.class.logger ; end # Signals that a given alert (which is assumed to belong in this group) # has undergone a significant change. We resend this to every notify list. - # + # + # @param [Mauve::Alert] alert + # + # @return [Boolean] indicates success or failure of alert. def notify(alert) # # If there are no notifications defined. # if notifications.nil? logger.warn("No notifications found for #{self.inspect}") - return + return false end # @@ -136,12 +164,15 @@ module Mauve sent_to << notification.notify(alert, sent_to) end + return (sent_to.length > 0) end - # # This sorts by priority (urgent first), and then alphabetically, so the # first match is the most urgent. # + # @param [Mauve::AlertGroup] other + # + # @return [Integer] def <=>(other) [LEVELS.index(self.level), self.name] <=> [LEVELS.index(other.level), other.name] end diff --git a/lib/mauve/authentication.rb b/lib/mauve/authentication.rb index 74c6780..d0d4596 100644 --- a/lib/mauve/authentication.rb +++ b/lib/mauve/authentication.rb @@ -5,10 +5,19 @@ require 'timeout' module Mauve + # + # Base class for authentication. + # class Authentication ORDER = [] + # Autenticates a user. + # + # @param [String] login + # @param [String] password + # + # @return [FalseClass] Always returns false. def authenticate(login, password) raise ArgumentError.new("Login must be a string, not a #{login.class}") if String != login.class raise ArgumentError.new("Password must be a string, not a #{password.class}") if String != password.class @@ -19,18 +28,28 @@ module Mauve false end + # @return [Log4r::Logger] def logger self.class.logger end - + + # @return [Log4r::Logger] def self.logger @logger ||= Log4r::Logger.new(self.to_s) end + # This calls all classes in the ORDER array one by one. If all classes + # fail, a 5 second sleep rate-limits authentication attempts. + # + # @param [String] login + # @param [String] password + # + # @return [Boolean] Success or failure. + # def self.authenticate(login, password) result = false - ORDER.each do |klass| + ORDER.any? do |klass| auth = klass.new result = begin @@ -41,10 +60,9 @@ module Mauve false end - if true == result - logger.info "Authenticated #{login} using #{auth.class.to_s}" - break - end + logger.info "Authenticated #{login} using #{auth.class.to_s}" if true == result + + result end unless true == result @@ -59,12 +77,20 @@ module Mauve end + # This is the Bytemark authentication mechansim. + # class AuthBytemark < Authentication Mauve::Authentication::ORDER << self + # Set up the Bytemark authenticator # - # TODO: allow configuration of where the server is. + # @todo allow configuration of where the server is. + # + # @param [String] srv Authentication server name + # @param [String] port Port overwhich authentication should take place + # + # @return [Mauve::AuthBytemark] # def initialize (srv='auth.bytemark.co.uk', port=443) raise ArgumentError.new("Server must be a String, not a #{srv.class}") if String != srv.class @@ -72,10 +98,16 @@ module Mauve @srv = srv @port = port @timeout = 7 + + self end - ## Not really needed. - def ping () + # Tests to see if a server is alive, alive-o. + # + # @deprecated Not really needed. + # + # @return [Boolean] + def ping begin Timeout.timeout(@timeout) do s = TCPSocket.open(@srv, @port) @@ -90,6 +122,12 @@ module Mauve return false end + # Authenticate against the Bytemark server + # + # @param [String] login + # @param [String] password + # + # @return [Boolean] def authenticate(login, password) super @@ -111,10 +149,19 @@ module Mauve end + # This is the local authentication mechansim, i.e. against the values in the + # Mauve config file. + # class AuthLocal < Authentication Mauve::Authentication::ORDER << self + # Authenticate against the local configuration + # + # @param [String] login + # @param [String] password + # + # @return [Boolean] def authenticate(login,password) super Digest::SHA1.hexdigest(password) == Mauve::Configuration.current.people[login].password diff --git a/lib/mauve/calendar_interface.rb b/lib/mauve/calendar_interface.rb index abce824..cf515d1 100644 --- a/lib/mauve/calendar_interface.rb +++ b/lib/mauve/calendar_interface.rb @@ -8,25 +8,20 @@ module Mauve # Interface to the Bytemark calendar. # - # Nota Bene that this does not include a chaching mechanism. Some caching - # is implemented in the Person object. - # - # @see Mauve::Person - # @author yann Golanski. class CalendarInterface class << self + # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.to_s) end # Gets a list of ssologin on support. # - # Class method. - # # @param [String] url A Calendar API url. - # @return [Array] A list of all the username on support. + # + # @return [Array] A list of all the usernames on support. def get_users_on_support(url) result = do_get_with_cache(url) @@ -41,10 +36,9 @@ module Mauve # Check to see if the user is on support. # - # Class method. - # # @param [String] url A Calendar API url. # @param [String] usr User single sign on. + # # @return [Boolean] True if on support, false otherwise. def is_user_on_support?(url, usr) return get_users_on_support(url).include?(usr) @@ -56,6 +50,7 @@ module Mauve # # @param [String] url A Calendar API url. # @param [String] usr User single sign on. + # # @return [Boolean] True if on holiday, false otherwise. def is_user_on_holiday?(url) result = do_get_with_cache(url) @@ -72,7 +67,7 @@ module Mauve # Grab a URL from the wide web. # - # @TODO: boot this in its own class since list of ips will need it too. + # @todo boot this in its own class since list of ips will need it too. # # @param [String] uri -- a URL # @return [String or nil] -- the contents of the URI or nil if an error has been encountered. @@ -136,6 +131,12 @@ module Mauve return nil end + # This does HTTP fetches with a 5 minute cache + # + # @param [String] url + # @param [Time] cache_until + # + # @return [String or nil] def do_get_with_cache(url, cache_until = Time.now + 5.minutes) @cache ||= {} diff --git a/lib/mauve/configuration.rb b/lib/mauve/configuration.rb index 56f5e7e..a74165b 100644 --- a/lib/mauve/configuration.rb +++ b/lib/mauve/configuration.rb @@ -4,26 +4,52 @@ require 'mauve/mauve_time' module Mauve - ## Configuration object for Mauve. - # - # - # @TODO Write some more documentation. This is woefully inadequate. + # Configuration object for Mauve. This is used as the context in + # Mauve::ConfigurationBuilder. # class Configuration class << self + # The current configuration + # @param [Mauve::Configuration] + # @return [Mauve::Configuration] attr_accessor :current end - + + # The Server instance + # @return [Mauve::Server] attr_accessor :server + + # The last AlertGroup to be configured + # @return [Mauve::AlertGroup] attr_accessor :last_alert_group + + # Notification methods + # @return [Hash] attr_reader :notification_methods + + # People + # @return [Hash] attr_reader :people + + # Alert groups + # @return [Array] attr_reader :alert_groups + + # People lists + # @return [Hash] attr_reader :people_lists + + # The source lists + # @return [Hash] attr_reader :source_lists - + + # + # Set up a base config. + # def initialize + @server = nil + @last_alert_group = nil @notification_methods = {} @people = {} @people_lists = Hash.new{|h,k| h[k] = Mauve::PeopleList.new(k)} diff --git a/lib/mauve/configuration_builder.rb b/lib/mauve/configuration_builder.rb index fb3c781..1d8d960 100644 --- a/lib/mauve/configuration_builder.rb +++ b/lib/mauve/configuration_builder.rb @@ -5,9 +5,11 @@ require 'mauve/people_list' require 'mauve/source_list' module Mauve + # + # This is the top-level configuration builder + # class ConfigurationBuilder < ObjectBuilder - # # This overwrites the default ObjectBuilder initialize method, such that # the context is set as a new configuration # @@ -16,19 +18,31 @@ module Mauve # FIXME: need to test blocks that are not immediately evaluated end + # Adds a source list + # + # @param [String] label + # @param [Array] list + # + # @return [Array] the whole source list for label def source_list(label, *list) _logger.warn "Duplicate source_list '#{label}'" if @result.source_lists.has_key?(label) @result.source_lists[label] += list end + # Adds a people list + # + # @param [String] label + # @param [Array] list + # + # @return [Array] the whole people list for label def people_list(label, *list) _logger.warn("Duplicate people_list '#{label}'") if @result.people_lists.has_key?(label) @result.people_lists[label] += list end - # # Have to use the method _logger here, cos logger is defined as a builder elsewhere. # + # @return [Log4r::Logger] def _logger @logger ||= Log4r::Logger.new(self.class.to_s) end diff --git a/lib/mauve/configuration_builders.rb b/lib/mauve/configuration_builders.rb index 03d666e..7ff6ac9 100644 --- a/lib/mauve/configuration_builders.rb +++ b/lib/mauve/configuration_builders.rb @@ -1,6 +1,15 @@ - require 'mauve/configuration_builders/logger' require 'mauve/configuration_builders/notification_method' require 'mauve/configuration_builders/person' require 'mauve/configuration_builders/server' require 'mauve/configuration_builders/alert_group' + +module Mauve + # + # This is where all the builders for the various configuration stanzas are + # kept. + # + module ConfigurationBuilders + + end +end diff --git a/lib/mauve/configuration_builders/alert_group.rb b/lib/mauve/configuration_builders/alert_group.rb index 9561d4d..c652de8 100644 --- a/lib/mauve/configuration_builders/alert_group.rb +++ b/lib/mauve/configuration_builders/alert_group.rb @@ -7,59 +7,81 @@ require 'mauve/notification' module Mauve module ConfigurationBuilders - + # + # This corresponds to a new "notify" clause an the alert_group config. + # class Notification < ObjectBuilder - def builder_setup(*who) - who = who.map do |username| - #raise BuildException.new("You haven't declared who #{username} is") unless - # @context.people[username] - #@context.people[username] - if @context.people[username] - @context.people[username] - elsif @context.people_lists[username] - @context.people_lists[username] - else - raise BuildException.new("You have not declared who #{username} is") + # Sets up the notification + # + # @param [Array] who List of usernames or people_lists to notify + # @raise [ArgumentError] if a username doesn't exist. + # + # @return [Mauve::Notification] New notification instance. + def builder_setup(*who) + who = who.map do |username| + #raise BuildException.new("You haven't declared who #{username} is") unless + # @context.people[username] + #@context.people[username] + if @context.people[username] + @context.people[username] + elsif @context.people_lists[username] + @context.people_lists[username] + else + raise ArgumentError.new("You have not declared who #{username} is") + end end + @result = Mauve::Notification.new(who, @context.last_alert_group.level) end - @result = Mauve::Notification.new(who, @context.last_alert_group.level) - end - - is_attribute "every" - is_block_attribute "during" - ##is_attribute "hours_in_day" - ##is_attribute "unacknowledged" - end - - class AlertGroup < ObjectBuilder - - def builder_setup(name=anonymous_name) - @result = Mauve::AlertGroup.new(name) - @context.last_alert_group = @result + + is_attribute "every" + is_block_attribute "during" end - is_block_attribute "includes" - is_attribute "acknowledgement_time" - is_attribute "level" + # This corresponds to a new alert_group clause + # + class AlertGroup < ObjectBuilder + + # Sets up the alert group, and sets the last_alert_group context. + # + # @param [String] name Name of the new alert group + # + # @return [Mauve::AlertGroup] New alert group instance + def builder_setup(name="anonymous_name") + @result = Mauve::AlertGroup.new(name) + @context.last_alert_group = @result + end - is_builder "notify", Notification + is_block_attribute "includes" + is_attribute "acknowledgement_time" + is_attribute "level" + is_builder "notify", Mauve::ConfigurationBuilders::Notification + + # Method called after the notify clause has been sorted. Adds new + # notification clause to the result. + # + # @param [Mauve::Notification] notification + # + def created_notify(notification) + @result.notifications ||= [] + @result.notifications << notification + end - def created_notify(notification) - @result.notifications ||= [] - @result.notifications << notification end end - end - - # this should live in AlertGroup but can't due to + # These constants define the levels available for alert groups. + # + # This should live in AlertGroup but can't due to # http://briancarper.net/blog/ruby-instance_eval_constant_scoping_broken # module AlertGroupConstants + # Urgent level URGENT = :urgent + # Normal level NORMAL = :normal + # Low level LOW = :low end @@ -70,9 +92,14 @@ module Mauve is_builder "alert_group", ConfigurationBuilders::AlertGroup + # Called after an alert group is created. Checks to make sure that no more than one alert group shares a name. + # + # @param [Mauve::AlertGroup] alert_group The new AlertGroup + # @raise [ArgumentError] if an AlertGroup with the same name already exists. + # def created_alert_group(alert_group) name = alert_group.name - raise BuildException.new("Duplicate alert_group '#{name}'") unless @result.alert_groups.select { |g| g.name == name }.empty? + raise ArgumentError.new("Duplicate alert_group '#{name}'") unless @result.alert_groups.select { |g| g.name == name }.empty? @result.alert_groups << alert_group end diff --git a/lib/mauve/configuration_builders/logger.rb b/lib/mauve/configuration_builders/logger.rb index 3f60dfe..d757d88 100644 --- a/lib/mauve/configuration_builders/logger.rb +++ b/lib/mauve/configuration_builders/logger.rb @@ -7,31 +7,48 @@ module Mauve class LoggerOutputter < ObjectBuilder + # Set up a Log4r::Outputter + # + # @param [String] outputter Outputter basic class name, like Stderr, Stdin, File. + # def builder_setup(outputter) @outputter = outputter.capitalize+"Outputter" begin Log4r.const_get(@outputter) - rescue + rescue NameError require "log4r/outputter/#{@outputter.downcase}" end - @outputter_name = "Mauve-"+5.times.collect{rand(36).to_s(36)}.join + @outputter_name = anonymous_name @args = {} end + # The new outputter + # + # @return [Log4r::Outputter] def result @result ||= Log4r.const_get(@outputter).new("Mauve", @args) end + # Set the formatter for this outputter (see Log4r::PatternFormatter for + # allowed patterns). SyntaxError is caught in the ObjectBuilder#parse + # method. + # + # @param [String] f The format + # + # @return [Log4r::PatternFormatter] def format(f) result.formatter = Log4r::PatternFormatter.new(:pattern => f) end - # # This is needed to be able to pass arbitrary arguments to Log4r - # outputters. + # outputters. Missing methods / bad arguments are caught in the + # ObjectBuilder#parse method. + # + # @param [String] name + # @param [Object] value # def method_missing(name, value=nil) if value.nil? @@ -47,38 +64,59 @@ module Mauve is_builder "outputter", LoggerOutputter + # Set up the new logger + # def builder_setup @result = Log4r::Logger['Mauve'] || Log4r::Logger.new('Mauve') @default_format = nil @default_level = Log4r::RootLogger.instance.level end + # Set the default format. Syntax erros are caught in the + # ObjectBuilder#parse method. + # + # @param [String] f Any format pattern allowed by Log4r::PatternFormatter. + # + # @return [Log4r::PatternFormatter] def default_format(f) - begin - @default_formatter = Log4r::PatternFormatter.new(:pattern => f) - rescue SyntaxError - raise BuildException.new "Bad log format #{f.inspect}" - end + @default_formatter = Log4r::PatternFormatter.new(:pattern => f) + # # Set all current outputters # result.outputters.each do |o| o.formatter = @default_formatter if o.formatter.is_a?(Log4r::DefaultFormatter) end + + @default_formatter end + # Set the default log level. + # + # @param [Integer] l The log level. + # @raise [ArgumentError] If the log level is bad + # + # @return [Integer] The log level set. def default_level(l) if Log4r::Log4rTools.valid_level?(l) @default_level = l else - raise "Bad default level set for the logger #{l}.inspect" + raise ArgumentError.new "Bad default level set for the logger #{l.inspect}" end result.outputters.each do |o| o.level = @default_level if o.level == Log4r::RootLogger.instance.level end + + @default_level end + # This is called once an outputter has been created. It sets the default + # formatter and level, if these have been already set. + # + # @param [Log4r::Outputter] outputter Newly created outputter. + # + # @return [Log4r::Outputter] The adjusted outputter. def created_outputter(outputter) # # Set the formatter and level for any newly created outputters @@ -92,7 +130,6 @@ module Mauve end result.outputters << outputter -# result.outputter.write("Created logger") end end end @@ -104,10 +141,15 @@ module Mauve module LoggerConstants Log4r.define_levels(*Log4r::Log4rConfig::LogLevels) # ensure levels are loaded + # Debug logging DEBUG = Log4r::DEBUG + # Info logging INFO = Log4r::INFO + # Warn logging WARN = Log4r::WARN + # Error logging ERROR = Log4r::ERROR + # Fatal logging FATAL = Log4r::FATAL end diff --git a/lib/mauve/configuration_builders/notification_method.rb b/lib/mauve/configuration_builders/notification_method.rb index 1192078..9596587 100644 --- a/lib/mauve/configuration_builders/notification_method.rb +++ b/lib/mauve/configuration_builders/notification_method.rb @@ -1,27 +1,46 @@ require 'mauve/notifiers' require 'mauve/configuration_builder' -# encoding: UTF-8 module Mauve module ConfigurationBuilders class NotificationMethod < ObjectBuilder + # + # Set up the notification. Missing notifiers are caught via NameError in + # the ObjectBuilder#parse method. + # + # @param [String] name Name of the notifier + # def builder_setup(name) - @notification_type = name.capitalize + notifiers_base = Mauve::Notifiers + + @notifier_type = notifiers_base.const_get(name.capitalize) + @name = name provider("Default") end + # This allows use of multiple notification providers, e.g. in the case of + # SMS. + # + # Missing providers are caught via NameError in the ObjectBuilder#parse + # method. + # def provider(name) - notifiers_base = Mauve::Notifiers - notifiers_type = notifiers_base.const_get(@notification_type) - @provider_class = notifiers_type.const_get(name) + @provider_class = @notifier_type.const_get(name) end + # Returns the result for this builder, depending on the configuration + # def result @result ||= @provider_class.new(@name) end - + + # This catches all methods available for a provider, as needed. + # + # Missing methods / bad arguments etc. are caught in the + # ObjectBuilder#parse method, via NoMethodError. + # def method_missing(name, value=nil) if value result.send("#{name}=".to_sym, value) @@ -38,6 +57,9 @@ module Mauve class ConfigurationBuilder < ObjectBuilder is_builder "notification_method", ConfigurationBuilders::NotificationMethod + # Method called after a notification method has been created to check for duplicate names. + # + # @raise [BuildException] when a duplicate notification method is found. def created_notification_method(notification_method) name = notification_method.name raise BuildException.new("Duplicate notification '#{name}'") if @result.notification_methods[name] diff --git a/lib/mauve/configuration_builders/person.rb b/lib/mauve/configuration_builders/person.rb index 1c012f2..c5314c5 100644 --- a/lib/mauve/configuration_builders/person.rb +++ b/lib/mauve/configuration_builders/person.rb @@ -15,29 +15,21 @@ module Mauve is_block_attribute "urgent" is_block_attribute "normal" is_block_attribute "low" - - def all(&block); urgent(&block); normal(&block); low(&block); end - - def password (pwd) - @result.password = pwd.to_s - end - - def holiday_url (url) - @result.holiday_url = url.to_s - end + is_attribute "password" + is_attribute "sms" + is_attribute "holiday_url" + is_attribute "email" + is_attribute "xmpp" + is_attribute "sms" - def email(e) - @result.email = e.to_s - end + # Sets the block for all levels of alert + # + # @param [Block] block + def all(&block); urgent(&block); normal(&block); low(&block); end - def xmpp(x) - @result.xmpp = x.to_s - end - - def sms(x) - @result.sms = x.to_s - end - + # Notification suppression hash + # + # @param [Hash] h def suppress_notifications_after(h) raise ArgumentError.new("notification_threshold must be specified as e.g. (10 => 1.minute)") unless h.kind_of?(Hash) @@ -54,9 +46,14 @@ module Mauve is_builder "person", ConfigurationBuilders::Person + # Method called once a person has been created to check for duplicate names + # + # @param [Mauve::Person] person + # @raise [ArgumentError] if a person has already been declared. + # def created_person(person) name = person.username - raise BuildException.new("Duplicate person '#{name}'") if @result.people[name] + raise ArgumentError.new("Duplicate person '#{name}'") if @result.people[name] # # Add a default notification threshold # diff --git a/lib/mauve/configuration_builders/server.rb b/lib/mauve/configuration_builders/server.rb index 3a9f5ec..c000144 100644 --- a/lib/mauve/configuration_builders/server.rb +++ b/lib/mauve/configuration_builders/server.rb @@ -5,74 +5,157 @@ require 'mauve/configuration_builder' module Mauve module ConfigurationBuilders + # + # This is the HTTP server + # class HTTPServer < ObjectBuilder + # The port the http server listens on is_attribute "port" + # The IP address the http server listens on. IPv6 is *NOT* OK. is_attribute "ip" + # Where all the templates are kept is_attribute "document_root" + # The secret for the cookies is_attribute "session_secret" + # The base URL of the server. is_attribute "base_url" + # Sets up a Mauve::HTTPServer singleton as the result + # + # @return [Mauve::HTTPServer] def builder_setup @result = Mauve::HTTPServer.instance end end + # + # This is the UDP server. + # class UDPServer < ObjectBuilder + # This is the port the server listens on is_attribute "port" + # This is the IP address the server listens on. IPv6 is OK! e.g. [::] for all addresses is_attribute "ip" + # This is the sleep interval for the UDP server. is_attribute "poll_every" + # Sets up a Mauve::UDPServer singleton as the result + # + # @return [Mauve::UDPServer] def builder_setup @result = Mauve::UDPServer.instance end end + # + # This is the thread that pulls packets from the queue for processing. + # class Processor < ObjectBuilder + # This is the interval between polls of the packet queue. is_attribute "poll_every" + # This is the timeout for the transmission cache, which allows duplicate packets to be discarded. is_attribute "transmission_cache_expire_time" + # Sets up a Mauve::Processor singleton as the result + # + # @return [Mauve::Processor] def builder_setup @result = Mauve::Processor.instance end end + # + # This is the Timer singleton. + # class Timer < ObjectBuilder + # + # This is the interval at which the Timer thread is run. This will limit + # the rate at which notifications can be sent, if set. + # is_attribute "poll_every" + # Sets up a Mauve::Timer singleton as the result + # + # @return [Mauve::Timer] def builder_setup @result = Mauve::Timer.instance end end class Notifier < ObjectBuilder + # + # This is the interval at which the notification queue is polled for new + # notifications to be sent. This will not have any rate-limiting effect. + # is_attribute "poll_every" + # Sets up a Mauve::Notifier singleton as the result + # + # @return [Mauve::Notifier] def builder_setup @result = Mauve::Notifier.instance end end + # + # This sends a mauve heartbeat to another Mauve instance elsewhere + # class Heartbeat < ObjectBuilder + # + # The destination for the heartbeat + # is_attribute "destination" + + # + # The detail field for the heartbeat + # is_attribute "detail" + + # + # The summary field for the heartbeat. + # is_attribute "summary" + + # + # How long to raise an alert after the last heartbeat. + # is_attribute "raise_after" + + # + # The interval between heartbeats + # is_attribute "send_every" - + + # Sets up a Mauve::Heartbeat singleton as the result + # + # @return [Mauve::Heartbeat] def builder_setup @result = Mauve::Heartbeat.instance end end class Pop3Server < ObjectBuilder + # + # The IP adderess the Pop3 server listens on + # is_attribute "ip" + + # + # The POP3 server port + # is_attribute "port" + # Sets up a Mauve::Pop3Server singleton as the result + # + # @return [Mauve::Pop3Server] def builder_setup @result = Mauve::Pop3Server.instance end end + # + # This is the main Server singleton. + # class Server < ObjectBuilder # # Set up second-level builders @@ -85,8 +168,19 @@ module Mauve is_builder "heartbeat", Heartbeat is_builder "pop3_server", Pop3Server + # + # The name of the server this instance of Mauve is running on + # is_attribute "hostname" + + # + # The database definition + # is_attribute "database" + + # + # The period of sleep during which no heartbeats are raised. + # is_attribute "initial_sleep" def builder_setup @@ -102,8 +196,12 @@ module Mauve is_builder "server", ConfigurationBuilders::Server + # This is called once the server object has been created. + # + # @raise [SyntaxError] if more than one server clause has been defined. + # def created_server(server) - raise BuildError.new("Only one 'server' clause can be specified") if @result.server + raise SyntaxError.new("Only one 'server' clause can be specified") if @result.server @result.server = server end diff --git a/lib/mauve/heartbeat.rb b/lib/mauve/heartbeat.rb index 39d3499..1fe2fab 100644 --- a/lib/mauve/heartbeat.rb +++ b/lib/mauve/heartbeat.rb @@ -3,17 +3,23 @@ require 'mauve/proto' require 'mauve/mauve_thread' require 'log4r' -# -# This class is responsible for sending a heartbeat to another mauve instance elsewhere. -# module Mauve + # + # This class is responsible for sending a heartbeat to another mauve instance elsewhere. + # class Heartbeat < MauveThread include Singleton + # + # Allow access to some basics. + # attr_reader :raise_after, :destination, :summary, :detail + # + # This sets up the Heartbeat singleton + # def initialize super @@ -24,6 +30,10 @@ module Mauve @poll_every = 60 end + # + # This is the time period after which an alert is raised by the remote Mauve instance. + # @param [Integer] i Seconds + # @return [Integer] Seconds def raise_after=(i) raise ArgumentError "raise_after must be an integer" unless i.is_a?(Integer) @raise_after = i @@ -31,25 +41,39 @@ module Mauve alias send_every= poll_every= + # Sets the summary of the heartbeat + # + # @param [String] s Summary def summary=(s) raise ArgumentError "summary must be a string" unless s.is_a?(String) @summary = s end + # Sets the detail of the heartbeat + # + # @param [String] d Detail def detail=(d) raise ArgumentError "detail must be a string" unless d.is_a?(String) @detail = d end + # Sets the destinantion Mauve instance + # + # @param [String] d Destination + # def destination=(d) raise ArgumentError "destination must be a string" unless d.is_a?(String) @destination = d end + # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.class.to_s) end + private + + # @private This is the main heartbeat loop. def main_loop # # Don't send if no destination set. diff --git a/lib/mauve/history.rb b/lib/mauve/history.rb index 76025a0..d9ab8e9 100644 --- a/lib/mauve/history.rb +++ b/lib/mauve/history.rb @@ -4,6 +4,10 @@ require 'mauve/alert' require 'log4r' module Mauve + # This is the look-up table for Alerts and History to allow one History to + # have many Alerts and vice-versa + # + # class AlertHistory include DataMapper::Resource @@ -15,6 +19,10 @@ module Mauve after :destroy, :remove_unreferenced_histories + # This is a horid migration to allow a move from an older version of Mauve + # without this table. + # + # def self.migrate! # # This copies the alert IDs from the old History table to the new AlertHistories thing, but only if there are no AertHistories @@ -45,12 +53,20 @@ module Mauve private + # This just removes histories that this AlertHistory used to refer to, if + # they have no other alerts associated with them + # + # def remove_unreferenced_histories self.history.destroy unless self.history.alerts.count > 0 end end + # This class keeps a history for Mauve. One History can relate to zero or + # more Alerts, allowing notes to be added. + # + # class History include DataMapper::Resource @@ -73,7 +89,6 @@ module Mauve protected - # # This cleans the HTML before saving. # def do_sanitize_html @@ -93,16 +108,21 @@ module Mauve end end - + # Update the created_at time on the object + # def set_created_at(context = :default) self.created_at = Time.now unless self.created_at.is_a?(Time) end public + # This adds an alert or an array of alerts to the cache of alerts + # associated with this model. # # Blasted datamapper not eager-loading my model. # + # @param [Array or Alert] a Array of Alerts or a single Alert + # @raise ArgumentError If +a+ is not an Array or an Alert def add_to_cached_alerts(a) @cached_alerts ||= [] if a.is_a?(Array) and a.all?{|m| m.is_a?(Alert)} @@ -114,15 +134,22 @@ module Mauve end end + # Find all the alerts for this History. This caches the alerts found. + # Call #reload to get rid of the cache. + # + # @return [Array] Alerts def alerts @cached_alerts ||= super end + # Reload the object, and clear the cache. + # def reload @cached_alerts = nil super end + # @return Log4r::Logger def logger Log4r::Logger.new self.class.to_s end diff --git a/lib/mauve/http_server.rb b/lib/mauve/http_server.rb index 72eb85a..583e6b5 100644 --- a/lib/mauve/http_server.rb +++ b/lib/mauve/http_server.rb @@ -21,26 +21,41 @@ require 'rack/handler/webrick' # Bodge up thin logging. # module Thin + # + # Bodge up thin logging. + # module Logging - + + # Log a message at "info" level + # + # @param [String] m def log(m=nil) # return if Logging.silent? logger = Log4r::Logger.new "Mauve::HTTPServer" logger.info(m || yield) end + # Log a message at "debug" level + # + # @param [String] m def debug(m=nil) # return unless Logging.debug? logger = Log4r::Logger.new "Mauve::HTTPServer" logger.debug(m || yield) end + # Log a trace at "debug" level + # + # @param [String] m def trace(m=nil) return unless Logging.trace? logger = Log4r::Logger.new "Mauve::HTTPServer" logger.debug(m || yield) end + # Log a message at "error" level + # + # @param [String] e def log_error(e=$!) logger = Log4r::Logger.new "Mauve::HTTPServer" logger.error(e) @@ -58,8 +73,16 @@ end # class RackErrorsProxy + # + # Set up the instance + # + # @param [Log4r::Logger] l The logger instance. + # def initialize(l); @logger = l; end + # Log a message at "error" level + # + # @param [String or Array] msg def write(msg) case msg when String then @logger.info(msg.chomp) @@ -72,6 +95,8 @@ class RackErrorsProxy alias_method :<<, :write alias_method :puts, :write + # no-op + # def flush; end end @@ -81,7 +106,7 @@ end module Mauve # - # API to control the web server + # The HTTP Server object # class HTTPServer < MauveThread @@ -90,6 +115,9 @@ module Mauve attr_reader :port, :ip, :document_root, :base_url attr_reader :session_secret + # + # Initialze the server + # def initialize super self.port = 1288 @@ -98,11 +126,18 @@ module Mauve self.session_secret = "%x" % rand(2**100) end + # Set the port + # + # @param [Intger] pr The port number between 1 and 2**16-1 + # @raise [ArgumentError] If the port is not valid def port=(pr) - raise ArgumentError, "port must be an integer between 0 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0 + raise ArgumentError, "port must be an integer between 1 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0 @port = pr end + # Set the listening IP address + # + # @param [String] i The IP def ip=(i) raise ArgumentError, "ip must be a string" unless i.is_a?(String) # @@ -112,6 +147,12 @@ module Mauve @ip = i end + # Set the document root. + # @param [String] d The directory where the templates etc are kept. + # @raise [ArgumentError] If d is not a string + # @raise [Errno::ENOTDIR] If d does not exist + # @raise [Errno::ENOTDIR] If d is not a directory + # def document_root=(d) raise ArgumentError, "document_root must be a string" unless d.is_a?(String) raise Errno::ENOENT, d unless File.exists?(d) @@ -120,6 +161,10 @@ module Mauve @document_root = d end + # Set the base URL + # + # @param [String] b The base URL, including https?:// + # @raise [ArgumentError] If b is not a string, or https?:// is missing def base_url=(b) raise ArgumentError, "base_url must be a string" unless b.is_a?(String) raise ArgumentError, "base_url should start with http:// or https://" unless b =~ /^https?:\/\// @@ -128,35 +173,50 @@ module Mauve # @base_url = b.chomp("/") end - + + # Set the cookie session secret + # + # @param [String] s The secret + # @raise [ArgumentError] if s is not a string def session_secret=(s) raise ArgumentError, "session_secret must be a string" unless s.is_a?(String) @session_secret = s end - def main_loop - unless @server and @server.running? - # - # Sessions are kept for 8 days. - # - @server = ::Thin::Server.new(@ip, @port, Rack::Session::Cookie.new(WebInterface.new, {:key => "mauvealert", :secret => @session_secret, :expire_after => 8.weeks}), :signals => false) - @server.start - end - end - + # Return the base_url + # + # @return [String] def base_url @base_url ||= "http://"+Server.instance.hostname end + # Stop the server + # def stop @server.stop if @server and @server.running? super end + # Stop the server, faster than #stop + # def join @server.stop! if @server and @server.running? super end + private + + # + # @private This is the main loop to keep the server going. + # + def main_loop + unless @server and @server.running? + # + # Sessions are kept for 8 days. + # + @server = ::Thin::Server.new(@ip, @port, Rack::Session::Cookie.new(WebInterface.new, {:key => "mauvealert", :secret => @session_secret, :expire_after => 8.weeks}), :signals => false) + @server.start + end + end end end diff --git a/lib/mauve/mauve_resolv.rb b/lib/mauve/mauve_resolv.rb index 6c97bef..c6460e3 100644 --- a/lib/mauve/mauve_resolv.rb +++ b/lib/mauve/mauve_resolv.rb @@ -1,12 +1,19 @@ require 'resolv-replace' -# -# -# - module Mauve + # + # This is just a quick class to resolve a hostname to all its IPs, IPv6 and IPv4. + # class MauveResolv + class << self + + # Get all IPs for a host, both IPv6 and IPv4. ResolvError and + # ResolvTimeout are both rescued. + # + # @param [String] host The hostname + # @return [Array] Array of IP addresses, as Strings. + # def get_ips_for(host) record_types = %w(A AAAA) ips = [] @@ -25,6 +32,7 @@ module Mauve ips end + # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.to_s) end diff --git a/lib/mauve/mauve_thread.rb b/lib/mauve/mauve_thread.rb index 2191d58..42f41a2 100644 --- a/lib/mauve/mauve_thread.rb +++ b/lib/mauve/mauve_thread.rb @@ -3,18 +3,39 @@ require 'singleton' module Mauve + # + # This is a class to wrap our threads that have processing loops. + # + # The thread is kept in a wrapper to allow it to be frozen and thawed at + # convenient times. + # class MauveThread + # + # The sleep interval between runs of the main loop. Defaults to 5 seconds. + # attr_reader :poll_every + # + # Set the thread up + # def initialize @thread = nil end + # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.class.to_s) end + # Set the sleep interval between runs of the main loop. This can be + # anything greater than or equal to zero. If a number less than zero gets + # entered, it will be increased to zero. + # + # @param [Numeric] i The number of seconds to sleep + # @raise [ArgumentError] If +i+ is not numeric + # @return [Numeric] + # def poll_every=(i) raise ArgumentError.new("poll_every must be numeric") unless i.is_a?(Numeric) # @@ -28,60 +49,20 @@ module Mauve @poll_every = i end - def run_thread(interval = 5.0) - # - # Good to go. - # - @thread = Thread.current - self.state = :starting - - @poll_every ||= interval - # - # Make sure we get a number. - # - @poll_every = 5 unless @poll_every.is_a?(Numeric) - - rate_limit = 0.1 - - while self.state != :stopping do - - self.state = :started if self.state == :starting - - # - # Schtop! - # - if self.state == :freezing - self.state = :frozen - Thread.stop - self.state = :started - end - - yield_start = Time.now.to_f - - yield - - # - # Ah-ha! Sleep with a break clause. Make sure we poll every @poll_every seconds. - # - ((@poll_every.to_f - Time.now.to_f + yield_start.to_f)/rate_limit). - round.to_i.times do - - break if self.should_stop? - - # - # This is a rate-limiting step - # - Kernel.sleep rate_limit - end - end - - self.state = :stopped - end - + # This determines if a thread should stop + # + # @return [Boolean] def should_stop? [:freezing, :stopping].include?(self.state) end + # This is the current state of the thread. It can be one of + # [:stopped, :starting, :started, :freezing, :frozen, :stopping, :killing, :killed] + # + # If the thread is not alive it will be +:stopped+. + # + # @return [Symbol] One of [:stopped, :starting, :started, :freezing, + # :frozen, :stopping, :killing, :killed] def state if self.alive? @thread.key?(:state) ? @thread[:state] : :unknown @@ -90,9 +71,18 @@ module Mauve end end + # This sets the state of a thread. It also records the last time the + # thread changed status. + # + # @param [Symbol] s One of [:stopped, :starting, :started, :freezing, + # :frozen, :stopping, :killing, :killed] + # @raise [ArgumentError] if +s+ is not a valid symbol or the thread is not + # ready + # @return [Symbol] the current thread state. + # def state=(s) - raise "Bad state for mauve_thread #{s.inspect}" unless [:stopped, :starting, :started, :freezing, :frozen, :stopping, :killing, :killed].include?(s) - raise "Thread not ready yet." unless @thread.is_a?(Thread) + raise ArgumentError, "Bad state for mauve_thread #{s.inspect}" unless [:stopped, :starting, :started, :freezing, :frozen, :stopping, :killing, :killed].include?(s) + raise ArgumentError, "Thread not ready yet." unless @thread.is_a?(Thread) unless @thread[:state] == s @thread[:state] = s @@ -103,6 +93,9 @@ module Mauve @thread[:state] end + # This returns the time of the last state change, or nil if the thread is dead. + # + # @return [Time or Nilclass] def last_state_change if self.alive? and @thread.key?(:last_state_change) return @thread[:last_state_change] @@ -111,6 +104,8 @@ module Mauve end end + # This asks the thread to freeze at the next opportunity. + # def freeze self.state = :freezing @@ -119,10 +114,16 @@ module Mauve logger.warn("Thread has not frozen!") unless @thread.stop? end + # This returns true if the thread has frozen successfully. + # + # @return [Boolean] def frozen? self.stop? and self.state == :frozen end + # This starts the thread. It wakes it up if it is alive, or starts it from + # fresh if it is dead. + # def run if self.alive? # Wake up if we're stopped. @@ -132,7 +133,7 @@ module Mauve else @logger = nil Thread.new do - self.run_thread { self.main_loop } + run_thread { main_loop } end end end @@ -140,15 +141,23 @@ module Mauve alias start run alias thaw run + # This checks to see if the thread is alive + # + # @return [Boolean] def alive? @thread.is_a?(Thread) and @thread.alive? end + # This checks to see if the thread is stopped + # + # @return [Boolean] def stop? self.alive? and @thread.stop? end - def join(ok_exceptions=[]) + # This joins the thread + # + def join @thread.join if @thread.is_a?(Thread) end @@ -156,15 +165,24 @@ module Mauve # @thread.raise(ex) # end + # This returns the thread's backtrace + # + # @return [Array or Nilclass] def backtrace @thread.backtrace if @thread.is_a?(Thread) end + # This restarts the thread + # + # def restart self.stop self.start end + # This stops the thread + # + # def stop self.state = :stopping @@ -183,16 +201,79 @@ module Mauve alias exit stop + # This kills the thread -- faster than #stop + # def kill self.state = :killing @thread.kill self.state = :killed end + # This returns the thread itself. + # + # @return [Thread] def thread @thread end + + private + + # This is the main run loop for the thread. In here are all the calls + # allowing use to freeze and thaw the thread neatly. + # + # This thread will run untill the thread state is changed to :stopping. + # + def run_thread(interval = 5.0) + # + # Good to go. + # + @thread = Thread.current + self.state = :starting + + @poll_every ||= interval + # + # Make sure we get a number. + # + @poll_every = 5 unless @poll_every.is_a?(Numeric) + + rate_limit = 0.1 + + while self.state != :stopping do + + self.state = :started if self.state == :starting + + # + # Schtop! + # + if self.state == :freezing + self.state = :frozen + Thread.stop + self.state = :started + end + + yield_start = Time.now.to_f + + yield + + # + # Ah-ha! Sleep with a break clause. Make sure we poll every @poll_every seconds. + # + ((@poll_every.to_f - Time.now.to_f + yield_start.to_f)/rate_limit). + round.to_i.times do + + break if self.should_stop? + + # + # This is a rate-limiting step + # + Kernel.sleep rate_limit + end + end + + self.state = :stopped + end + end end diff --git a/lib/mauve/mauve_time.rb b/lib/mauve/mauve_time.rb index 2d9e293..d2949d9 100644 --- a/lib/mauve/mauve_time.rb +++ b/lib/mauve/mauve_time.rb @@ -1,14 +1,19 @@ require 'date' require 'time' - -# Seconds, minutes, hours, days, and weeks... More than that, we -# really should not need it. +# +# Extra methods for integer to calculate periods in seconds. +# class Integer + # @return [Integer] def seconds; self; end + # @return [Integer] n minutes of seconds def minutes; self*60; end + # @return [Integer] n hours of seconds def hours; self*3600; end + # @return [Integer] n days of seconds def days; self*86400; end + # @return [Integer] n weeks of seconds def weeks; self*604800; end alias_method :day, :days alias_method :hour, :hours @@ -16,36 +21,22 @@ class Integer alias_method :week, :weeks end -class Date - def to_time - Time.parse(self.to_s) - end -end - -class DateTime - def to_time - Time.parse(self.to_s) - end - - def to_s_relative(*args) - self.to_s_relative(*args) - end - - def to_s_human - self.to_s_human - end - - def in_x_hours(*args) - self.in_x_hours(*args) - end - -end - +# +# Extra methods for Time. +# class Time + # + # Method to work out when something is due, with different classes of hours. + # + # @param [Integer] n The number of hours in the future + # @param [String] type One of wallclock, working, or daytime. + # + # @return [Time] + # def in_x_hours(n, type="wallclock") t = self.dup # - # Do this in minutes rather than hours + # Do this in seconds rather than hours # n = n.to_i*3600 @@ -103,34 +94,42 @@ class Time t end + # Test to see if we're in working hours. The working day is from 8.30am until + # 17:00 # - # The working day is from 8.30am until 17:00 - # + # @return [Boolean] def working_hours? (1..5).include?(self.wday) and ((9..16).include?(self.hour) or (self.hour == 8 && self.min >= 30)) end + # Test to see if it is currently daytime. The daytime day is 14 hours long # - # The daytime day is 14 hours long - # + # @return [Boolean] def daytime_hours? (8..21).include?(self.hour) end + + # We're always in wallclock hours # - # The daytime day is 14 hours long - # + # @return [true] def wallclock_hours? true end + # Test to see if we're in the DEAD ZONE! This is from 3 - 6am every day. # - # In the DEAD ZONE! - # + # @return [Boolean] def dead_zone? (3..6).include?(self.hour) end - + + # Format the time as a string, relative to +now+ + # + # @param [Time] now The time we're using as a base + # @raise [ArgumentError] if +now+ is not a Time + # @return [String] + # def to_s_relative(now = Time.now) # # Make sure now is the correct class diff --git a/lib/mauve/notification.rb b/lib/mauve/notification.rb index 00ce989..920f1b1 100644 --- a/lib/mauve/notification.rb +++ b/lib/mauve/notification.rb @@ -6,7 +6,7 @@ module Mauve # This class provides an execution context for the code found in 'during' # blocks in the configuration file. This code specifies when an alert # should cause notifications to be generated, and can access @time and - # @alert variables. There are also some helper methods to provide + # \@alert variables. There are also some helper methods to provide # oft-needed functionality such as hours_in_day. # # e.g. to send alerts only between 10 and 11 am: @@ -15,7 +15,7 @@ module Mauve # # ... later on ... # - # DuringRunner.new(Time.now, my_alert, &during).inside? + # DuringRunner.new(Time.now, my_alert, &during).now? # # ... or to ask when an alert will next be cued ... # @@ -28,6 +28,14 @@ module Mauve attr_reader :time, :alert, :during + # + # Sets up the class + # + # @param [Time] time The time we're going to test + # @param [Mauve::Alert or Nilclass] alert The alert that we can use in tests. + # @param [Proc] during The Proc that is evaluated inside the instance to decide if now is now! + # @raise [ArgumentError] if +time+ is not a Time + # def initialize(time, alert=nil, &during) raise ArgumentError.new("Must supply time (not #{time.inspect})") unless time.is_a?(Time) @time = time @@ -36,14 +44,21 @@ module Mauve @logger = Log4r::Logger.new "Mauve::DuringRunner" end + # This evaluates the +during+ block, returning the result. # - # - # + # @param [Time] t Set the time at which to evaluate the +during+ block. + # @return [Boolean] def now?(t=@time) @test_time = t - res = instance_eval(&@during) + instance_eval(&@during) ? true : false end + # This finds the next occurance of the +during+ block evaluating to true. + # It returns nil if an occurence cannot be found within the next 8 days. + # + # @param [Integer] after Skip time forward this many seconds before starting + # @return [Time or nil] + # def find_next(after = 0) t = @time+after # @@ -86,18 +101,28 @@ module Mauve protected + # Returns true if the current hour is in the list of hours given. + # + # @param [Array] hours List of hours (as Integers) + # @return [Boolean] def hours_in_day(*hours) @test_time = @time if @test_time.nil? x_in_list_of_y(@test_time.hour, hours.flatten) - end - + end + + # Returns true if the current day is in the list of days given + # + # @param [Array] days List of days (as Integers) + # @return [Boolean] def days_in_week(*days) @test_time = @time if @test_time.nil? x_in_list_of_y(@test_time.wday, days.flatten) end - ## Return true if the alert has not been acknowledged within a certain time. - # + # Tests if the alert has not been acknowledged within a certain time. + # + # @param [Integer] seconds Number of seconds + # @return [Boolean] def unacknowledged(seconds) @test_time = @time if @test_time.nil? @alert && @@ -106,6 +131,11 @@ module Mauve (@test_time - @alert.raised_at) >= seconds end + # Checks to see if x is contained in y + # + # @param [Array] y Array to search for +x+ + # @param [Object] x + # @return [Boolean] def x_in_list_of_y(x,y) y.any? do |range| if range.respond_to?("include?") @@ -116,16 +146,17 @@ module Mauve end end + # Test to see if we're in working hours. See Time#working_hours? + # + # @return [Boolean] def working_hours? @test_time = @time if @test_time.nil? @test_time.working_hours? end - # Return true in the dead zone between 3 and 7 in the morning. + # Test to see if we're in the dead zone. See Time#dead_zone? # - # Nota bene that this is used with different times in the reminder section. - # - # @return [Boolean] Whether now is a in the dead zone or not. + # @return [Boolean] def dead_zone? @test_time = @time if @test_time.nil? @test_time.dead_zone? @@ -139,20 +170,31 @@ module Mauve # class Notification < Struct.new(:people, :level, :every, :during, :list) - def to_s - "#<Notification:of #{people.map { |p| p.username }.join(',')} at level #{level} every #{every}>" - end - - attr_reader :thread_list - + # Set up a new notification + # + # @param [Array] people List of Mauve::Person to notify + # @param [Symbol] level Level at which to notify def initialize(people, level) self.level = level self.every = 300 self.people = people end + # @return [String] + def to_s + "#<Notification:of #{people.map { |p| p.username }.join(',')} at level #{level} every #{every}>" + end + + # @return Log4r::Logger def logger ; Log4r::Logger.new self.class.to_s ; end + # Push a notification on to the queue for this alert. The Mauve::Notifier + # will then pop it off and do the notification in a separate thread. + # + # @param [Mauve::Alert or Mauve::AlertChanged] alert The alert in question + # @param [Array] already_sent_to A list of people that have already received this alert. + # + # @return [Array] The list of people that have received this alert. def notify(alert, already_sent_to = []) if people.nil? or people.empty? @@ -187,7 +229,13 @@ module Mauve return already_sent_to end - + + # Work out when this notification should next get sent. Nil will be + # returned if the alert is not raised. + # + # @param [Mauve::Alert] alert The alert in question + # @return [Time or nil] The time a reminder should get sent, or nil if it + # should never get sent again. def remind_at_next(alert) return nil unless alert.raised? diff --git a/lib/mauve/notifier.rb b/lib/mauve/notifier.rb index 2853485..14e188c 100644 --- a/lib/mauve/notifier.rb +++ b/lib/mauve/notifier.rb @@ -4,33 +4,45 @@ require 'mauve/notifiers/xmpp' module Mauve + # The Notifier is reponsible for popping notifications off the + # notification_buffer run by the Mauve::Server instance. This ensures that + # notifications are sent in a separate thread to the main processing / + # updating threads, and stops notifications delaying updates. + # + # class Notifier < MauveThread include Singleton - def main_loop - # - # Cycle through the buffer. - # - sz = Server.notification_buffer_size + # Stop the notifier thread. This just makes sure that all the + # notifications in the buffer have been sent before closing the XMPP + # connection. + # + def stop + super - # Thread.current[:notification_threads] ||= [] - logger.info "Sending #{sz} alerts" if sz > 0 - - sz.times do - person, *args = Server.notification_pop - - # - # Nil person.. that's craaazy too! - # - next if person.nil? + # + # Flush the queue. + # + main_loop - person.send_alert(*args) + if Configuration.current.notification_methods['xmpp'] + Configuration.current.notification_methods['xmpp'].close end + end - def start - if Configuration.current.notification_methods['xmpp'] + private + + # This is the main loop that is executed in the thread. + # + # + def main_loop + + # + # Make sure we're connected to the XMPP server if needed on every iteration. + # + if Configuration.current.notification_methods['xmpp'] and !Configuration.current.notification_methods['xmpp'].ready? # # Connect to XMPP server # @@ -41,37 +53,48 @@ module Mauve # # Ignore people without XMPP stanzas. # - next unless person.xmpp + next unless person.xmpp + + # + # Can't do this unless we're ready. + # + next unless xmpp.ready? # # For each JID, either ensure they're on our roster, or that we're in # that chat room. # jid = if xmpp.is_muc?(person.xmpp) - xmpp.join_muc(person.xmpp) + xmpp.join_muc(person.xmpp) else xmpp.ensure_roster_and_subscription!(person.xmpp) end Configuration.current.people[username].xmpp = jid unless jid.nil? end + end - super - end + # + # Cycle through the buffer. + # + sz = Server.notification_buffer_size - def stop - super + logger.info "Sending #{sz} alerts" if sz > 0 # - # Flush the queue. + # Empty the buffer, one notification at a time. # - main_loop + sz.times do + person, *args = Server.notification_pop + + # + # Nil person.. that's craaazy too! + # + next if person.nil? - if Configuration.current.notification_methods['xmpp'] - Configuration.current.notification_methods['xmpp'].close + person.send_alert(*args) end - end end diff --git a/lib/mauve/notifiers.rb b/lib/mauve/notifiers.rb index 7ba1d71..7276091 100644 --- a/lib/mauve/notifiers.rb +++ b/lib/mauve/notifiers.rb @@ -1,6 +1,13 @@ -# encoding: UTF-8 require 'mauve/notifiers/email' require 'mauve/notifiers/sms_default' require 'mauve/notifiers/sms_aql' require 'mauve/notifiers/xmpp' +module Mauve + # + # This is where the various notifiers are kept. + # + module Notifiers + + end +end diff --git a/lib/mauve/notifiers/email.rb b/lib/mauve/notifiers/email.rb index 3134135..8efdbec 100644 --- a/lib/mauve/notifiers/email.rb +++ b/lib/mauve/notifiers/email.rb @@ -30,10 +30,14 @@ module Mauve def logger @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"") - end def send_alert(destination, alert, all_alerts, conditions = {}) + + logger.debug [destination, alert, all_alerts, conditions] + + return false if destination.nil? + message = prepare_message(destination, alert, all_alerts, conditions) args = [@server, @port] args += [@username, @password, @login_method.to_sym] if @login_method diff --git a/lib/mauve/notifiers/xmpp.rb b/lib/mauve/notifiers/xmpp.rb index 5ffe8f4..ae9734f 100644 --- a/lib/mauve/notifiers/xmpp.rb +++ b/lib/mauve/notifiers/xmpp.rb @@ -85,15 +85,25 @@ module Mauve @client = nil end + # The logger instance + # + # @return [Log4r::Logger] def logger # Give the logger a sane name @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"") end + # Sets the client's JID + # + # @param [String] jid The JID required. + # @return [Jabber::JID] The client JID. def jid=(jid) @jid = JID.new(jid) end - + + # Connects to the XMPP server, and sets up the roster + # + # @return [Jabber::Client, NilClass] The connected client, or nil in the case of failure def connect logger.debug "Starting connection to #{@jid}" @@ -156,13 +166,14 @@ module Mauve @client = nil end - # - # Kills the processor thread - # def stop @client.stop end - + + # + # Closes the XMPP connection, if possible. Sets @client to nil. + # + # @return [NilClass] def close @closing = true if @client @@ -177,27 +188,32 @@ module Mauve @client = nil end + # Determines if the client is ready. + # + # @return [Boolean] def ready? @client.is_a?(Jabber::Client) and @client.is_connected? end - # - # Takes an alert and converts it into a message. - # - def convert_alert_to_message(alert) - end - # Attempt to send an alert using XMPP. - # +destination+ is the JID you're sending the alert to. This should be - # a bare JID in the case of an individual, or muc:<room>@<server> for - # chatrooms (XEP0045). The +alert+ object is turned into a pretty - # message and sent to the destination as a message, if the +conditions+ - # are met. all_alerts are currently ignored. # - # The only suported condition at the moment is :if_presence => [choices] - # which checks whether the jid in question has a presence matching one - # or more of the choices - see +check_jid_has_presence+ for options. - + # @param [String] destination The JID you're sending the alert to. This should be + # a bare JID in the case of an individual, or +muc:room@server+ for + # chatrooms (XEP0045). + # + # @param [Mauve::Alert] alert This is turned into a pretty + # message and sent to the destination as a message, if +conditions+ + # are met. + # + # @param [Array] all_alerts Currently ignored. + # + # @param [Hash] conditions Conditions that determine if an alert should be sent + # + # @option conditions [Array] :if_presence Checks whether the jid in question + # has a presence matching one or more of the choices - see + # Mauve::Notifiers::Xmpp::Default#check_jid_has_presence for options. + # + # @return [Boolean] def send_alert(destination, alert, all_alerts, conditions = {}) destination_jid = JID.new(destination) @@ -650,7 +666,6 @@ EOF # :unavailable - - jabber status is away, dnd or xa # :unknown - don't know (not in roster) # - # For MUCs: TODO # Returns true if at least one of the presence specifiers for the jid # is met, false otherwise. Note that if the alerter can't see the alertee's # presence, only 'unknown' will match - generally, you'll want [:online, :unknown] diff --git a/lib/mauve/people_list.rb b/lib/mauve/people_list.rb index b15db3f..6b2ba33 100644 --- a/lib/mauve/people_list.rb +++ b/lib/mauve/people_list.rb @@ -4,14 +4,18 @@ require 'mauve/calendar_interface' module Mauve - # Stores a list of name. + # Stores a list of Mauve::Person + # # - # @author Yann Golanski class PeopleList attr_reader :label, :list - # Default contrustor. + # Create a new list + # + # @param [String] label The name of the list + # @raise [ArgumentError] if the label is not a string + # def initialize(label) raise ArgumentError, "people_list label must be a string" unless label.is_a?(String) @label = label @@ -20,6 +24,10 @@ module Mauve alias username label + # Append an Array or String to a list + # + # @param [Array or String] arr + # @return [Mauve::PeopleList] self def +(arr) case arr when Array @@ -44,15 +52,14 @@ module Mauve alias add_to_list + - # - # Set up the logger + # @return Log4r::Logger def logger @logger ||= Log4r::Logger.new self.class.to_s end - # # Return the array of people # + # @return [Array] def people l = list.collect do |name| diff --git a/lib/mauve/person.rb b/lib/mauve/person.rb index 0df31ee..acab8b7 100644 --- a/lib/mauve/person.rb +++ b/lib/mauve/person.rb @@ -6,7 +6,19 @@ module Mauve class Person < Struct.new(:username, :password, :holiday_url, :urgent, :normal, :low, :email, :xmpp, :sms) attr_reader :notification_thresholds, :last_pop3_login, :suppressed - + + # Set up a new Person + # + # @param [Hash] args The options for setting up the person + # @option args [String] :username The person's username + # @option args [String] :password The SHA1 sum of the person's password + # @option args [String] :holiday_url The URL that can be checked by Mauve::CalendarInterface#is_user_on_holiday? + # @option args [Proc] :urgent The block to execute when an urgent-level notification is issued + # @option args [Proc] :normal The block to execute when an normal-level notification is issued + # @option args [Proc] :low The block to execute when an low-level notification is issued + # @option args [String] :email The person's email address + # @option args [String] :sms The person's mobile number + # def initialize(*args) @notification_thresholds = nil @suppressed = false @@ -17,13 +29,19 @@ module Mauve super(*args) end + # @return Log4r::Logger def logger ; @logger ||= Log4r::Logger.new self.class.to_s ; end + # Determines if notifications to the user are currently suppressed + # + # @return [Boolean] def suppressed? ; @suppressed ; end + # Works out if a notification should be suppressed. If no parameters are supplied, it will # - # This - # + # @param [Time] Theoretical time of notification + # @param [Time] Current time. + # @return [Boolean] If suppression is needed. def should_suppress?(with_notification_at = nil, now = Time.now) return self.notification_thresholds.any? do |period, previous_alert_times| @@ -42,10 +60,10 @@ module Mauve end end + # The notification thresholds for this user + # + # @return [Hash] def notification_thresholds - # - # By default send 10 thresholds in a minute maximum - # @notification_thresholds ||= { } end @@ -54,6 +72,13 @@ module Mauve # class NotificationCaller + # Set up the notification caller + # + # @param [Mauve::Person] person + # @param [Mauve::Alert] alert + # @param [Array] other_alerts + # @param [Hash] base_conditions + # def initialize(person, alert, other_alerts, base_conditions={}) @person = person @alert = alert @@ -61,40 +86,52 @@ module Mauve @base_conditions = base_conditions end + # @return Log4r::Logger def logger ; @logger ||= Log4r::Logger.new self.class.to_s ; end + # This method makes sure things like +xmpp+ and +email+ work. # - # This method makes sure things like - # - # xmpp - # - # works + # @param [String] name The notification method to use + # @param [Array or Hash] args Extra conditions to pass to this notification method # + # @return [Boolean] if the notifcation has been successful def method_missing(name, *args) # + # Work out the notification method + # + notification_method = Configuration.current.notification_methods[name.to_s] + + @logger.warn "Notification method '#{name}' not defined (#{@person.username})" if notification_method.nil? + + # # Work out the destination # if args.first.is_a?(String) destination = args.pop - else + elsif @person.respond_to?(name) destination = @person.__send__(name) + else + destination = nil end + @logger.warn "#{name} destination for #{@person.username} not set" if destination.nil? + if args.first.is_a?(Array) conditions = @base_conditions.merge(args[0]) else conditions = @base_conditions end - notification_method = Configuration.current.notification_methods[name.to_s] - - raise NoMethodError.new("#{name} not defined as a notification method") unless notification_method - # Methods are expected to return true or false so the user can chain - # them together with || as fallbacks. So we have to catch exceptions - # and turn them into false. - # - res = notification_method.send_alert(destination, @alert, @other_alerts, conditions) + if notification_method and destination + # Methods are expected to return true or false so the user can chain + # them together with || as fallbacks. So we have to catch exceptions + # and turn them into false. + # + res = notification_method.send_alert(destination, @alert, @other_alerts, conditions) + else + res = false + end # # Log the result @@ -108,10 +145,12 @@ module Mauve end - # - # # Sends the alert # + # @param [Symbol] level Level at which the alert should be sent + # @param [Mauve::Alert] alert Alert we're notifiying about + # + # @return [Boolean] if the notification was successful def send_alert(level, alert) now = Time.now @@ -132,8 +171,6 @@ module Mauve return true end - # FIXME current_alerts is very slow. So much so it slows everything - # down. A lot. result = NotificationCaller.new( self, alert, @@ -143,7 +180,7 @@ module Mauve :was_suppressed => was_suppressed, } ).instance_eval(&__send__(level)) - if result + if [result].flatten.any? # # Remember that we've sent an alert # @@ -168,6 +205,7 @@ module Mauve # This is currently very CPU intensive, and slows things down a lot. So # I've commented it out when sending notifications. # + # @return [Array] alerts relevant to this person def current_alerts Alert.all_raised.select do |alert| my_last_update = AlertChanged.first(:person => username, :alert_id => alert.id) diff --git a/lib/mauve/pop3_server.rb b/lib/mauve/pop3_server.rb index 645fe0d..52f31d0 100644 --- a/lib/mauve/pop3_server.rb +++ b/lib/mauve/pop3_server.rb @@ -4,25 +4,41 @@ require 'digest/sha1' module Mauve # - # API to control the web server + # The POP3 server, where messages can also be read. # class Pop3Server < MauveThread include Singleton attr_reader :port, :ip - + + # Initialize the server + # + # Default port is 1110 + # Default IP is 0.0.0.0 + # def initialize super self.port = 1110 self.ip = "0.0.0.0" end + # + # Set the port + # + # @param [Integer] pr + # @raise [ArgumentError] if the port is not sane + # ~ def port=(pr) raise ArgumentError, "port must be an integer between 0 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0 @port = pr end + # + # Set the IP address. Unfortunately IPv6 is not OK. + # + # @param [String] i The IP address required. + # def ip=(i) raise ArgumentError, "ip must be a string" unless i.is_a?(String) # @@ -32,22 +48,15 @@ module Mauve @ip = i end - + + # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.class.to_s) end - def main_loop - unless @server and @server.running? - @server = Mauve::Pop3Backend.new(@ip.to_s, @port) - logger.info "Listening on #{@server.to_s}" - # - # The next statment doesn't return. - # - @server.start - end - end - + # + # This stops the server + # def stop if @server.running? @server.stop @@ -58,25 +67,50 @@ module Mauve super end + # + # This stops the server faster than stop + # def join @server.stop! if @server super end + private + + # + # This tarts the server, and keeps it going. + # + def main_loop + unless @server and @server.running? + @server = Mauve::Pop3Backend.new(@ip.to_s, @port) + logger.info "Listening on #{@server.to_s}" + # + # The next statment doesn't return. + # + @server.start + end + end + end + # + # This is the Pop3 Server itself. It is based on the Thin HTTP server, and hence EventMachine. + # class Pop3Backend < Thin::Backends::TcpServer + # + # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.class.to_s) end - # Connect the server + # Initialize a new connection to the server def connect @signature = EventMachine.start_server(@host, @port, Pop3Connection) end + # Disconnect the server, but only if EventMachine is still going. def disconnect # # Only do this if EventMachine is still going.. The http_server may have @@ -87,17 +121,25 @@ module Mauve end - + # + # This class represents and individual connection, and understands some POP3 + # commands. + # class Pop3Connection < EventMachine::Connection + # The username attr_reader :user + # Default CR+LF combo. CRLF = "\r\n" + # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.class.to_s) end + # This is called once the connection has been established. It says hello + # to the client, and resets the state. def post_init logger.info "New connection" send_data "+OK #{self.class.to_s} started" @@ -107,6 +149,11 @@ module Mauve @level = nil end + # This returns a list of commands allowed in a state. + # + # @param [Symbol] The state to query, defaults to the current state. + # @return [Array] An array of permitted comands. + # def permitted_commands(state=@state) case @state when :authorization @@ -118,6 +165,10 @@ module Mauve end end + # This returns a list of capabilities in a given state. + # + # @param [Symbol] The state to query, defaults to the current state. + # @return [Array] An array of capabilities. def capabilities(state=@state) case @state when :transaction @@ -129,6 +180,27 @@ module Mauve end end + # This method handles a command, and parses it. + # + # The following POP3 commands are understood: + # QUIT + # USER + # PASS + # STAT + # LIST + # RETR + # DELE + # NOOP + # RSET + # CAPA + # UIDL + # + # The command is checked against a list of permitted commands, given the + # state of the connection, and returns an error if the command is + # forbidden. + # + # @param [String] data The data to process. + # def receive_data (data) data.split(CRLF).each do |cmd| break if error? @@ -165,16 +237,39 @@ module Mauve end end end - + + # This sends the data back to the user. A CR+LF is joined to the end of + # the data. + # + # @param [String] d The data to send back. def send_data(d) d += CRLF super unless error? end + private + + # This deals with CAPA, returning a string of capabilities in the current + # connection state. + # + # @param [String] a The complete CAPA command sent by the client. + # def do_process_capa(a) send_data (["+OK Capabilities follow:"] + self.capabilities + ["."]).join(CRLF) end + # This deals with the USER command. + # + # Any of low, normal, urgent can be appended to the username, to select + # only alarms of that level to be shown. + # + # e.g. + # patrick+low + # + # will show only alerts of a LOW level. + # + # @param [String] s The complete USER command sent by the client. + # def do_process_user(s) allowed_levels = Mauve::AlertGroup::LEVELS.collect{|l| l.to_s} @@ -195,6 +290,10 @@ module Mauve end end + # This processes the PASS command. It uses the Mauve::Authenticate class + # to authenticate the user. Once authenticated, the state is set to :transaction. + # + # @param [String] s The complete PASS command sent by the client. def do_process_pass(s) if @user and s =~ /\APASS +(\S+)/ @@ -209,16 +308,27 @@ module Mauve end end + # + # This just sends an "ERR Unknown command" string back to the user. + # + # @param [String] a The complete command from the client that caused this error. def do_process_error(a) send_data "-ERR Unknown comand." end + # This does a NOOP. + # + # @param [String] a The complete NOOP command from the client. def do_process_noop(a) send_data "+OK Thanks." end + # Delete is processed as a NOOP alias do_process_dele do_process_noop + # This logs a user out, and closes the connection. The state is set to :update. + # + # @param [String] a The complete QUIT command from the client. def do_process_quit(a) @state = :update @@ -227,10 +337,17 @@ module Mauve close_connection_after_writing end + # This sends the number of messages, and their size back to the client. + # + # @param [String] a The complete STAT command from the client. def do_process_stat(a) send_data "+OK #{self.messages.length} #{self.messages.inject(0){|s,m| s+= m[1].length}}" end + # This sends a list of the messages back to the client. + # + # @param [String] a The complete LIST command from the client. + # def do_process_list(a) d = [] if a =~ /\ALIST +(\d+)\b/ @@ -249,6 +366,9 @@ module Mauve send_data d.join(CRLF) end + # This sends the UID of a message back to the client. + # + # @param [String] a The complete UIDL command from the client. def do_process_uidl(a) if a =~ /\AUIDL +(\d+)\b/ ind = $1.to_i @@ -267,6 +387,10 @@ module Mauve end end + # This retrieves a message for the client. + # + # @param [String] a The complete RETR command from the client. + # def do_process_retr(a) if a =~ /\ARETR +(\d+)\b/ ind = $1.to_i @@ -287,7 +411,11 @@ module Mauve end # - # These are the messages in the mailbox. + # These are the messages in the mailbox. It looks for the first 100 alert_changed, and formats them into emails, and returns an array of + # + # [alert_changed, email] + # + # @return [Array] Array of alert_changeds and emails. # def messages if @messages.empty? diff --git a/lib/mauve/processor.rb b/lib/mauve/processor.rb index d68c551..c8a1dfb 100644 --- a/lib/mauve/processor.rb +++ b/lib/mauve/processor.rb @@ -4,12 +4,23 @@ require 'mauve/mauve_thread' module Mauve + # + # This class is a singlton thread which pops updates off the + # Server#packet_buffer and processes them as alert updates. + # + # It is responsible for de-bouncing updates, i.e. ones with duplicate + # transmission IDs. + # class Processor < MauveThread include Singleton + # This is the time after which transmission IDs are expired. + # attr_reader :transmission_cache_expire_time + # Initialize the processor + # def initialize super # @@ -20,15 +31,58 @@ module Mauve @transmission_cache_checked_at = Time.now end + # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.class.to_s) end + # Set the expiry time + # + # @param [Integer] i The number of seconds after which transmission IDs are considered unseen. + # @raise [ArgumentError] If +i+ is not an Integer def transmission_cache_expire_time=(i) raise ArgumentError, "transmission_cache_expire_time must be an integer" unless i.is_a?(Integer) @transmission_cache_expire_time = i end + # This expries the transmission cache + # + # + def expire_transmission_id_cache + now = Time.now + # + # Only check once every minute. + # + return unless (now - @transmission_cache_checked_at) > 60 + + to_delete = [] + + @transmission_id_cache.each do |tid, received_at| + to_delete << tid if (now - received_at) > @transmission_cache_expire_time + end + + to_delete.each do |tid| + @transmission_id_cache.delete(tid) + end + + @transmission_cache_checked_at = now + end + + # This stops the processor, making sure all pending updates are saved. + # + def stop + super + + # + # flush the queue + # + main_loop + end + + private + + # This is the main loop that does the processing. + # def main_loop sz = Server.packet_buffer_size @@ -92,33 +146,7 @@ module Mauve Timer.instance.thaw if Timer.instance.frozen? end - def expire_transmission_id_cache - now = Time.now - # - # Only check once every minute. - # - return unless (now - @transmission_cache_checked_at) > 60 - - to_delete = [] - - @transmission_id_cache.each do |tid, received_at| - to_delete << tid if (now - received_at) > @transmission_cache_expire_time - end - - to_delete.each do |tid| - @transmission_id_cache.delete(tid) - end - - @transmission_cache_checked_at = now - end - - def stop - super - - # - # flush the queue - # - main_loop - end end + end + diff --git a/lib/mauve/sender.rb b/lib/mauve/sender.rb index 6685de5..c84dcb0 100644 --- a/lib/mauve/sender.rb +++ b/lib/mauve/sender.rb @@ -4,12 +4,40 @@ require 'socket' require 'mauve/mauve_resolv' require 'mauve/mauve_time' -module Mauve +module Mauve + # + # This is the class that send the Mauve packets. + # class Sender + # + # This is the default mauve receiving port. + # DEFAULT_PORT = 32741 include Resolv::DNS::Resource::IN - + + # Set up a new sender. It takes a list of destinations and uses DNS to + # resolve names to addresses. + # + # A destination can look like + # + # 1.2.3.4:5678 + # 1.2.3.4 + # [2001:1af:ba8::dead]:5678 + # [2001:1af:ba8::dead] + # mauve.host:5678 + # mauve.host + # + # If no port is specified, DEFAULT_PORT is used. + # + # If a hostname is used, SRV records are used, with the prefix + # +_mauvealert._udp+ to determine the real hostname and port to which + # alerts should be sent. + # + # Otherwise AAAA and A records are looked up. + # + # @param [Array] destinations List of destinations to which the update is to be sent. + # def initialize(*destinations) destinations = destinations.flatten @@ -101,9 +129,12 @@ module Mauve end + # Send an update. # - # Returns the number of packets sent. + # @param [Mauve::Proto] update The update to send + # @param [Integer] vebose The verbosity -- higher is more. # + # @return [Integer] the number of packets sent. def send(update, verbose=0) # diff --git a/lib/mauve/server.rb b/lib/mauve/server.rb index a217aab..41a76dc 100644 --- a/lib/mauve/server.rb +++ b/lib/mauve/server.rb @@ -25,11 +25,14 @@ module Mauve # THREAD_CLASSES = [UDPServer, HTTPServer, Pop3Server, Processor, Timer, Notifier, Heartbeat] - attr_reader :hostname, :database, :initial_sleep + attr_reader :hostname, :database, :initial_sleep attr_reader :packet_buffer, :notification_buffer, :started_at include Singleton + # Initialize the Server, setting up a blank configuration if no + # configuration has been created already. + # def initialize super @hostname = "localhost" @@ -51,29 +54,47 @@ module Mauve Configuration.current = Configuration.new if Mauve::Configuration.current.nil? end + # Set the hostname of this Mauve instance. + # + # @param [String] h The hostname def hostname=(h) raise ArgumentError, "hostname must be a string" unless h.is_a?(String) @hostname = h end + # Set the database + # + # @param [String] d The database def database=(d) raise ArgumentError, "database must be a string" unless d.is_a?(String) @database = d end - + + # Set the sleep period during which notifications about old alerts are + # suppressed. + # + # @param [Integer] s The initial sleep period. def initial_sleep=(s) raise ArgumentError, "initial_sleep must be numeric" unless s.is_a?(Numeric) @initial_sleep = s end + # Test to see if we should suppress alerts because we're in the initial sleep period + # + # @return [Boolean] def in_initial_sleep? Time.now < self.started_at + self.initial_sleep end + # return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new(self.class.to_s) end + # This sorts out the Server. It empties the notification and packet + # buffers. It configures and migrates the database. + # + # @return [NilClass] def setup # # @@ -103,15 +124,45 @@ module Mauve return nil end + # This sets up the server, and then starts the main loop. + # def start # self.state = :starting - self.setup + setup - self.run_thread { self.main_loop } + run_thread { main_loop } end alias run start + + # + # This stops the main loop, and all the threads that are defined in THREAD_CLASSES above. + # + def stop + if self.state == :stopping + # uh-oh already told to stop. + logger.error "Stop already called. Killing self!" + Kernel.exit 1 + end + + self.state = :stopping + + THREAD_CLASSES.each do |klass| + klass.instance.stop unless klass.instance.nil? + end + + thread_list = Thread.list + thread_list.delete(Thread.current) + + thread_list.each do |t| + t.exit + end + + self.state = :stopped + end + + private def main_loop thread_list = Thread.list @@ -185,29 +236,6 @@ module Mauve end end - def stop - if self.state == :stopping - # uh-oh already told to stop. - logger.error "Stop already called. Killing self!" - Kernel.exit 1 - end - - self.state = :stopping - - THREAD_CLASSES.each do |klass| - klass.instance.stop unless klass.instance.nil? - end - - thread_list = Thread.list - thread_list.delete(Thread.current) - - thread_list.each do |t| - t.exit - end - - self.state = :stopped - end - class << self @@ -218,17 +246,23 @@ module Mauve # processing them crash, the buffer itself is not lost with the thread. # - # - # These methods pop things on and off the packet_buffer - # + # Push a packet onto the back of the +packet_buffer+ + # + # @param [String] a Packet from the UDP server def packet_enq(a) instance.packet_buffer.push(a) end + # Shift a packet off the front of the +packet buffer+ + # + # @return [String] the oldest UDP packet def packet_deq instance.packet_buffer.shift end + # Returns the current length of the +packet_buffer+ + # + # @return [Integer} def packet_buffer_size instance.packet_buffer.size end @@ -236,17 +270,23 @@ module Mauve alias packet_push packet_enq alias packet_pop packet_deq + # Push a notification on to the back of the +notification_buffer+ # - # These methods pop things on and off the notification_buffer - # + # @param [Array] a Notification array, consisting of a Person and the args to Mauve::Person#send_alert def notification_enq(a) instance.notification_buffer.push(a) end + # Shift a notification off the front of the +notification_buffer+ + # + # @return [Array] Notification array, consisting of a Person and the args to Mauve::Person#send_alert def notification_deq instance.notification_buffer.shift end + # Return the current length of the +notification_buffer+ + # + # @return [Integer] def notification_buffer_size instance.notification_buffer.size end diff --git a/lib/mauve/source_list.rb b/lib/mauve/source_list.rb index caa9f9a..b8c89cc 100644 --- a/lib/mauve/source_list.rb +++ b/lib/mauve/source_list.rb @@ -9,20 +9,14 @@ module Mauve # A simple construct to match sources. # - # This class stores mamed lists of IP addresses. It stores them in a hash - # indexed by the name of the list. One can pass IPv4, IPv6 and hostnames - # as list elements but they will all be converted into IP addresses at - # the time of the list creation. - # # One can ask if an IPv4, IPv6, hostname or url (match on hostname only) is # contained within a list. If the query is not an IP address, it will be - # converted into one before the checks are made. + # converted into one as the checks are made. # # Note that the matching is greedy. When a hostname maps to several IP # addresses and only one of tbhose is included in the list, a match - # will occure. + # will occur. # - # @author Yann Golanski class SourceList attr_reader :label, :list @@ -37,6 +31,26 @@ module Mauve alias username label + # Adds a source onto the list. + # + # The source can be a string, or array of strings. Each one can be an IPv6 + # or IPv4 address or range, or a hostname. + # + # Hostnames can have *, or numeric ranges in their name. A '*' represents + # any character except ".". A range can be specified as 1..4, meaning 1, + # 2, 3 or 4. + # + # e.g. 1.2.3.4/24 + # 2001:dead::beef/64 + # app1..10.my-customer.com + # *.db.my-customer.com + # + # Hostnames are also resolved into IP addresses, and re-resolved every 30 + # minutes. + # + # @param [String or Array] l The source(s) to add. + # @return [SourceList] + # def +(l) arr = [l].flatten.collect do |h| # "*" means [^\.]+ @@ -52,12 +66,15 @@ module Mauve gsub(/\./, "\\."). gsub(/\*/, "[0-9a-z\\-]+") + "\\.?$") - elsif h.is_a?(String) and h =~ /^[0-9a-f\.:\/]+$/i + elsif h.is_a?(String) and h =~ /^[0-9a-f\.:]+(\/\d+)?$/i IPAddr.new(h) - else + elsif h.is_a?(String) or h.is_a?(Regexp) h + else + logger.warn "Cannot add #{h.inspect} to source list #{@label} as it is not a string or regular expression." + nil end - end.flatten + end.flatten.reject{|h| h.nil?} arr.each do |source| ## @@ -79,13 +96,26 @@ module Mauve alias add_to_list + + # @return [Log4r::Logger] def logger @logger ||= Log4r::Logger.new self.class.to_s end - ## + # # Return whether or not a list contains a source. - ## + # + # First the hostname is checked for a URI, using URI#parse, and then the + # hostname is extracted from there. If that fails, the original hostname + # is used. + # + # Next we check against our list, including all IPs for any hostnames in + # that list. + # + # If nothing is found, the hostname is then resolved to its IPs, and we + # check to see if those IPs are in our list. + # + # @param [String] host The host to look for. + # @return [Boolean] def includes?(host) # # Redo resolution every thirty minutes @@ -133,14 +163,20 @@ module Mauve ips.any?{|ip| list_ip.include?(ip)} end + return false end + # + # Resolve all hostnames in the list to IP addresses. + # + # @return [Array] The new list. + # def resolve @last_resolved_at = Time.now new_list = @list.collect do |host| if host.is_a?(String) - ips = [host] + MauveResolv.get_ips_for(host).collect{|i| IPAddr.new(i)} + [host] + MauveResolv.get_ips_for(host).collect{|i| IPAddr.new(i)} else host end diff --git a/lib/mauve/timer.rb b/lib/mauve/timer.rb index a00d66d..5a43b60 100644 --- a/lib/mauve/timer.rb +++ b/lib/mauve/timer.rb @@ -7,6 +7,9 @@ require 'log4r' module Mauve + # + # This is the thread that looks for reminders and heartbeat alerts to poll. + # class Timer < MauveThread include Singleton @@ -20,6 +23,14 @@ module Mauve super end + private + + # This is the trigger for heartbeats and reminders. + # + # It looks up the next event, and sleeps until it is due. If an update + # comes in (via the processor) it is broken out of its sleep, and starts + # again when woken up. + # def main_loop # # Get the next alert. diff --git a/lib/mauve/udp_server.rb b/lib/mauve/udp_server.rb index 080a04b..e3362e8 100644 --- a/lib/mauve/udp_server.rb +++ b/lib/mauve/udp_server.rb @@ -8,12 +8,22 @@ require 'ipaddr' module Mauve + # + # This is the thread that accepts the packets. + # class UDPServer < MauveThread include Singleton attr_reader :ip, :port + # Yup. Creates a new UDPServer. + # + # Defaults: + # * listening IP: 127.0.0.1 + # * listening port: 32741 + # * polls every: 0 seconds + # def initialize # # Set up some defaults. @@ -26,16 +36,50 @@ module Mauve super end + # + # This sets the IP which the server will listen on. + # + # @param [String] i The new IP + # @return [IPAddr] + # def ip=(i) raise ArgumentError, "ip must be a string" unless i.is_a?(String) @ip = IPAddr.new(i) end + # Sets the listening port + # + # @param [Integer] pr The new port + # @return [Integer] + # def port=(pr) raise ArgumentError, "port must be an integer between 0 and #{2**16-1}" unless pr.is_a?(Integer) and pr < 2**16 and pr > 0 @port = pr end + + # This stops the UDP server, by signalling to the thread to stop, and + # sending a zero-length packet to the socket. + # + def stop + @stop = true + # + # Triggers loop to close socket. + # + UDPSocket.open(Socket.const_get(@socket.addr[0])).send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.nil? or @socket.closed? + + super + end + + private + # + # This opens the socket for listening. + # + # It tries to increase the default receiving buffer to 10M, and will warn + # if this fails to increase the buffer at all. + # + # @return [Nilclass] + # def open_socket # # Specify the family when opening the socket. @@ -48,7 +92,7 @@ module Mauve @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, 10*1024*1024) new = @socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF).unpack("i").first - raise "Could not increase Socket::SO_RCVBUF. Had #{old} ended up with #{new}!" if old > new + logger.warn "Could not increase Socket::SO_RCVBUF. Had #{old} ended up with #{new}!" if old > new logger.debug("Successfully increased Socket::SO_RCVBUF from #{old} to #{new}.") @@ -57,6 +101,8 @@ module Mauve logger.info("Opened socket on #{@ip.to_s}:#{@port}") end + # This closes the socket. IOErrors are caught and logged. + # def close_socket return if @socket.nil? or @socket.closed? @@ -71,6 +117,13 @@ module Mauve logger.info("Closed socket") end + # This is the main loop. It receives from the UDP port, and records the + # time the packet arrives. It then pushes the packet onto the Server's + # packet buffer for the processor to pick up later. + # + # If a zero-length packet is received, and the thread has been signalled to + # stop, then the socket is closed, and the thread exits. + # def main_loop return if self.should_stop? @@ -99,7 +152,7 @@ module Mauve # If we get a zero length packet, and we've been flagged to stop, we stop! # if packet.first.length == 0 and self.should_stop? - self.close_socket + close_socket return end @@ -109,16 +162,6 @@ module Mauve Server.packet_push([packet[0], packet[1], received_at]) end - def stop - @stop = true - # - # Triggers loop to close socket. - # - UDPSocket.open(Socket.const_get(@socket.addr[0])).send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.nil? or @socket.closed? - - super - end - end end diff --git a/lib/mauve/version.rb b/lib/mauve/version.rb index 56ac522..2b35608 100644 --- a/lib/mauve/version.rb +++ b/lib/mauve/version.rb @@ -1,5 +1,10 @@ +# +# This is Mauve! +# module Mauve + # + # Current version VERSION="3.5.8" end diff --git a/lib/object_builder.rb b/lib/object_builder.rb index a455b3e..8f338ff 100644 --- a/lib/object_builder.rb +++ b/lib/object_builder.rb @@ -45,13 +45,21 @@ class ObjectBuilder attr_reader :result attr_accessor :block_result - + + # Generates a new builder + # + # @param [ObjectBuidler] context The level of the builder + # @param [Array] args The arguments to pass on to the builder_setup + # def initialize(context, *args) @context = context @result = nil builder_setup(*args) end + # Generates an anonymous name + # + # @return [String] def anonymous_name @@sequence ||= 0 # not inherited, don't want it to be @@sequence += 1 @@ -60,6 +68,15 @@ class ObjectBuilder class << self + # Defines a new builder + # + # @param [String] word The builder's name + # @param [Class] clazz The Class the builder represents + # + # @macro [attach] is_builder + # @return The +$1+ builder. + # + # @return [NilClass] def is_builder(word, clazz) define_method(word.to_sym) do |*args, &block| builder = clazz.new(*([@context] + args)) @@ -72,59 +89,84 @@ class ObjectBuilder end end end + + return nil end # FIXME: implement is_builder_deferred to create object at end of block? + # Defines a new block attribute + # @param [String] word The block attribute's name + # @macro [attach] is_block_attribute + # @return [NilClass] Allows use of the +$1+ word to define a block. def is_block_attribute(word) define_method(word.to_sym) do |*args, &block| @result.__send__("#{word}=".to_sym, block) end end - + + # Defines a new attribute + # @param [String] word The attribute's name + # @macro [attach] is_attribute + # @return [NilClass] Allows use of the +$1+ word to set an attribute. + # def is_attribute(word) define_method(word.to_sym) do |*args, &block| @result.__send__("#{word}=".to_sym, args[0]) end end + # Defines a new boolean attribute + # @param [String] word The boolean attribute's name + # @macro [attach] is_flag_attribute + # @return [NilClass] Allows use of the +$1+ word to set an boolean attribute. def is_flag_attribute(word) define_method(word.to_sym) do |*args, &block| @result.__send__("#{word}=".to_sym, true) end end + # Loads a new file + # + # @param [String] file def load(file) parse(File.read(file), file) end + # Parses a string + # + # @param [String] string The string to parse + # @param [String] file The filename that the string was read from, for helpful error messages. + # + # @raise [BuildException] When an expected exception is raised + # @raise [NameError] + # @raise [SyntaxError] + # @raise [ArgumentError] + # + # @return [ObjectBuidler] The def parse(string, file="string") builder = self.new - begin - builder.instance_eval(string, file) - rescue NameError => ex - # - # Ugh. Catch NameError and re-raise as a BuildException - # - f,l = ex.backtrace.first.split(":").first(2) - if f == file - build_ex = BuildException.new "Unknown word `#{ex.name}' in #{file} at line #{l}" - build_ex.set_backtrace ex.backtrace - raise build_ex - else - raise ex - end - rescue SyntaxError, ArgumentError => ex - if ex.backtrace.find{|l| l =~ /^#{file}:(\d+):/} - build_ex = BuildException.new "#{ex.message} in #{file} at line #{$1}" - build_ex.set_backtrace ex.backtrace - raise build_ex - else - raise ex - end - end - + builder.instance_eval(string, file) builder.result + rescue NameError, NoMethodError => ex + # + # Ugh. Catch NameError and re-raise as a BuildException + # + if ex.backtrace.find{|l| l =~ /^#{file}:(\d+):/} + build_ex = BuildException.new "Unknown word `#{ex.name}' in #{file} at line #{$1}" + build_ex.set_backtrace ex.backtrace + raise build_ex + else + raise ex + end + rescue SyntaxError, ArgumentError => ex + if ex.backtrace.find{|l| l =~ /^#{file}:(\d+):/} + build_ex = BuildException.new "#{ex.message} in #{file} at line #{$1}" + build_ex.set_backtrace ex.backtrace + raise build_ex + else + raise ex + end end def inherited(*args) diff --git a/test/tc_mauve_notification.rb b/test/tc_mauve_notification.rb index 4243874..0b3bc04 100644 --- a/test/tc_mauve_notification.rb +++ b/test/tc_mauve_notification.rb @@ -39,12 +39,12 @@ class TcMauveDuringRunner < Mauve::UnitTest def test_now? alert = Alert.new time = Time.now - during = Proc.new { @test_time } + during = Proc.new { Time.now == @test_time } dr = DuringRunner.new(time, alert, &during) - assert_equal(time, dr.now?) - assert_equal(time+3600, dr.now?(time+3600)) + assert_equal(true, dr.now?) + assert_equal(false, dr.now?(time+3600)) assert_equal(time, dr.time) end |