apt_control 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/apt_control.rb CHANGED
@@ -2,16 +2,22 @@
2
2
  require 'inifile'
3
3
  require 'listen'
4
4
  require 'logger'
5
+ require 'celluloid'
6
+
7
+ # silence celluloid until we have our own logger
8
+ Celluloid.logger = nil
5
9
 
6
10
  module AptControl
7
11
 
8
12
  require 'apt_control/exec'
13
+ require 'apt_control/actors'
9
14
  require 'apt_control/jabber'
15
+ require 'apt_control/bot'
10
16
  require 'apt_control/control_file'
11
17
  require 'apt_control/apt_site'
12
18
  require 'apt_control/build_archive'
13
19
  require 'apt_control/package_states'
14
- require 'apt_control/includer'
20
+ require 'apt_control/commands'
15
21
 
16
22
  class Version
17
23
  include Comparable
@@ -19,7 +25,7 @@ module AptControl
19
25
  attr_reader :major, :minor, :bugfix, :debian
20
26
 
21
27
  def self.parse(string)
22
- match = /([0-9]+)(?:\.([0-9]+))?(?:\.([0-9]+))?(?:-(.+))?/.match(string)
28
+ match = /^([0-9]+)(?:\.([0-9]+))?(?:\.([0-9]+))?(?:-(.+))?$/.match(string)
23
29
  match && new(*(1..4).map { |i| match[i] }) or raise "could not parse #{string}"
24
30
  end
25
31
 
@@ -0,0 +1,41 @@
1
+ #
2
+ # Explicitly proxy some objects to have actor proxies - perhaps celluloid already
3
+ # has this facility?
4
+ #
5
+ # The rationale is that I want to leave the underlying objects 'clean' so I
6
+ # can test them individually without having to worry about celluloid magic
7
+ #
8
+ module AptControl::Actors
9
+
10
+ module ProxiedClassMethods
11
+ def proxy(method)
12
+ proxy_class.class_eval """\
13
+ def #{method}(*args, &block)
14
+ @proxied_object.#{method}(*args, &block)
15
+ end
16
+ """
17
+ end
18
+
19
+ attr_reader :proxy_class
20
+
21
+ end
22
+
23
+ class ActorProxy
24
+ include Celluloid
25
+ def initialize(proxied_object)
26
+ @proxied_object = proxied_object
27
+ end
28
+ end
29
+
30
+ module Proxied
31
+ def self.included(other_class)
32
+ @proxy_class = Class.new(ActorProxy)
33
+ other_class.instance_variable_set('@proxy_class', @proxy_class)
34
+ other_class.extend(ProxiedClassMethods)
35
+ end
36
+
37
+ def actor
38
+ @actor_proxy ||= self.class.proxy_class.new(self)
39
+ end
40
+ end
41
+ end
@@ -2,6 +2,9 @@ module AptControl
2
2
  # represents the reprepro apt site that we query and include packages in to
3
3
  class AptSite
4
4
  include Exec::Helpers
5
+ include Actors::Proxied
6
+ proxy :include!
7
+ proxy :included_version
5
8
 
6
9
  def initialize(apt_site_dir, logger)
7
10
  @apt_site_dir = apt_site_dir
@@ -0,0 +1,121 @@
1
+ module AptControl
2
+ class Bot
3
+
4
+ # as a module for testing
5
+ module ArgHelpers
6
+ def split_args(args_string)
7
+ # there is probably some code out there that can do this better, maybe
8
+ # go out and find it if it proves troublsome :)
9
+ args_string.scan(/[^ '"]+|'[^']+'|"[^"]+"/).map do |str|
10
+ str.gsub(/'|"/, '')
11
+ end
12
+ end
13
+ end
14
+
15
+ include ArgHelpers
16
+
17
+ module ClassMethods
18
+ def method_added(meth)
19
+ super
20
+ if match = /^handle_(.+)$/.match(meth.to_s)
21
+ handlers << match[1]
22
+ end
23
+ end
24
+
25
+ def handlers
26
+ @handlers ||= []
27
+ end
28
+ end
29
+
30
+ self.extend(ClassMethods)
31
+ include Actors::Proxied
32
+ proxy :on_message
33
+
34
+ def initialize(dependencies)
35
+ @jabber = dependencies.fetch(:jabber)
36
+ @package_states = dependencies.fetch(:package_states)
37
+ @logger = dependencies.fetch(:logger)
38
+ @command_start = dependencies.fetch(:command_start)
39
+ @include_cmd = dependencies.fetch(:include_cmd)
40
+ @control_file = dependencies.fetch(:control_file)
41
+
42
+ @bot_pattern = /#{Regexp.escape(@command_start)}\: ([^ ]+)(?: (.+))?/
43
+ @logger.info("looking for messages starting with #{@command_start}")
44
+ @logger.debug(" match pattern: #{@bot_pattern}")
45
+ end
46
+
47
+ def on_message(text)
48
+ return unless match = @bot_pattern.match(text)
49
+
50
+ command, args = [match[1], match[2]]
51
+
52
+ handler = self.class.handlers.include?(command) or
53
+ return print_help("unknown command '#{command}'")
54
+
55
+ args = split_args(args || '')
56
+ begin
57
+ self.send("handle_#{command}", args)
58
+ rescue => e
59
+ begin ; send_message("error: #{e}") ; rescue => e ; end
60
+ @logger.error("error handling #{command}")
61
+ @logger.error(e)
62
+ end
63
+ end
64
+
65
+ def send_message(msg)
66
+ @jabber.async.send_message(msg)
67
+ end
68
+
69
+ def print_help(message)
70
+ send_message(message)
71
+ send_message("Send commands with '#{@command_start} COMMAND [ARGS...]'")
72
+ send_message("Available commands: #{self.class.handlers.join(' ')}")
73
+ end
74
+
75
+ def handle_status(args)
76
+ dist = args[0]
77
+ package_name = args[1]
78
+
79
+ found = @package_states.map do |package_state|
80
+ next if dist && dist != package_state.dist.name
81
+ next if package_name && package_name != package_state.package_name
82
+
83
+ send_message(package_state.status_line)
84
+ true
85
+ end.compact
86
+
87
+ send_message("no packages found: distribution => #{dist.inspect}, package_name => #{package_name.inspect} ") if found.empty?
88
+ end
89
+
90
+ def handle_include(args)
91
+ performed = @include_cmd.run(@package_states) do |state, version|
92
+ send_message("#{state.dist.name} #{state.package_name} #{state.included} => #{version}")
93
+ true
94
+ end
95
+
96
+ send_message("no packages were included") if performed.empty?
97
+ end
98
+
99
+ def handle_reload(args)
100
+ @control_file.reload!
101
+ send_message("control file reloaded")
102
+ end
103
+
104
+ def handle_set(args)
105
+ set_command.run(*args)
106
+ end
107
+
108
+ def handle_promote(args)
109
+ promote_command.run(*args)
110
+ end
111
+
112
+ def set_command
113
+ AptControl::Commands::Set.new(control_file: @control_file)
114
+ end
115
+
116
+ def promote_command
117
+ AptControl::Commands::Promote.new(control_file: @control_file,
118
+ package_states: @package_states)
119
+ end
120
+ end
121
+ end
@@ -50,6 +50,7 @@ module AptControl
50
50
  rescue => e
51
51
  @logger.error("Couldn't parse version string: #{s}")
52
52
  @logger.error(e)
53
+ nil
53
54
  end
54
55
  }.compact
55
56
  Package.new(name, versions)
@@ -102,6 +103,7 @@ module AptControl
102
103
  end
103
104
 
104
105
  def add_version(version)
106
+ raise unless version.is_a?(AptControl::Version)
105
107
  @versions << version
106
108
  end
107
109
 
@@ -36,16 +36,19 @@ module AptControl
36
36
  require 'apt_control/cli/status'
37
37
  require 'apt_control/cli/watch'
38
38
  require 'apt_control/cli/include'
39
+ require 'apt_control/cli/set'
40
+ require 'apt_control/cli/promote'
39
41
  end
40
42
 
41
43
  module Common
42
44
  # FIXME tidy up with some meta magic
43
45
  def package_states ; ancestor(Root).package_states ; end
44
- def includer ; ancestor(Root).includer ; end
46
+ def new_include_cmd(options={}) ; ancestor(Root).new_include_cmd(options) ; end
45
47
  def apt_site ; ancestor(Root).apt_site ; end
46
48
  def control_file ; ancestor(Root).control_file ; end
47
49
  def build_archive ; ancestor(Root).build_archive ; end
48
- def notifier ; ancestor(Root).notifier ; end
50
+ def jabber ; ancestor(Root).jabber ; end
51
+ def jabber_enabled? ; ancestor(Root).jabber_enabled? ; end
49
52
  def notify(msg) ; ancestor(Root).notify(msg) ; end
50
53
  def validate_config! ; ancestor(Root).validate_config! ; end
51
54
  def logger ; ancestor(Root).logger ; end
@@ -71,7 +74,7 @@ module AptControl
71
74
 
72
75
  DEFAULT_CONFIG_FILE_LOCATION = '/etc/apt_control/config.yaml'
73
76
 
74
- config :log_file, "File to send log output to, defaults to stdout", :required => false
77
+ config :log_file, "File to send log output to, defaults to /dev/null", :required => false
75
78
  config :apt_site_dir, "Directory containing apt files"
76
79
  config :control_file, "Path to control file containing inclusion rules"
77
80
  config :build_archive_dir, "Directory containing debian build files"
@@ -138,10 +141,13 @@ YAML file containing a single hash of key value/pairs for each option.
138
141
  config[key] or raise Climate::ExitException, "Error: you must supply all jabber options if jabber is enabled"
139
142
  end
140
143
  end
144
+
145
+ Celluloid.logger = logger
141
146
  end
142
147
 
143
148
  def logger
144
- @logger ||= Logger.new(config[:log_file] || STDOUT).tap do |logger|
149
+ log_file = config[:log_file] || '/dev/null'
150
+ @logger ||= Logger.new(log_file == 'STDOUT' ? STDOUT : log_file).tap do |logger|
145
151
  logger.level = Logger::DEBUG
146
152
  end
147
153
  end
@@ -164,13 +170,21 @@ YAML file containing a single hash of key value/pairs for each option.
164
170
  @build_archive ||= BuildArchive.new(config[:build_archive_dir], logger)
165
171
  end
166
172
 
167
- def notifier
168
- @notify ||= Jabber.new(:jid => config[:jabber_id], :logger => logger,
169
- :password => config[:jabber_password], :room_jid => config[:jabber_chatroom_id])
173
+ def jabber
174
+ @jabber ||= Jabber.new(:jid => config[:jabber_id], :logger => logger,
175
+ :password => config[:jabber_password], :room_jid => config[:jabber_chatroom_id],
176
+ :enabled => jabber_enabled?)
177
+ end
178
+
179
+ def jabber_enabled?
180
+ config[:jabber_enabled].to_s == 'true'
170
181
  end
171
182
 
172
- def includer
173
- @includer ||= Includer.new(apt_site, build_archive)
183
+ def new_include_cmd(options={})
184
+ defaults = {apt_site: apt_site, build_archive: build_archive}
185
+ options = options.merge(defaults)
186
+
187
+ AptControl::Commands::Include.new(options)
174
188
  end
175
189
 
176
190
  class FSListenerFactory
@@ -186,6 +200,8 @@ YAML file containing a single hash of key value/pairs for each option.
186
200
  if disable_inotify
187
201
  listener.force_polling(true)
188
202
  listener.polling_fallback_message(false)
203
+ else
204
+ listener.force_adapter(Listen::Adapters::Linux)
189
205
  end
190
206
 
191
207
  listener.change(&on_change)
@@ -200,9 +216,8 @@ YAML file containing a single hash of key value/pairs for each option.
200
216
 
201
217
  def notify(message)
202
218
  logger.info("notify: #{message}")
203
- return unless config[:jabber_enabled].to_s == 'true'
204
219
  begin
205
- notifier.send_message(message)
220
+ jabber.actor.async.send_message(message)
206
221
  rescue => e
207
222
  logger.error("Unable to send notification to jabber: #{e}")
208
223
  logger.error(e)
@@ -10,7 +10,8 @@ that the control file will allow"""
10
10
  def run
11
11
  validate_config!
12
12
 
13
- includer.perform_for_all(package_states) do |state, version|
13
+
14
+ new_include_cmd.run(package_states) do |state, version|
14
15
  if options[:noop]
15
16
  puts "#{state.dist.name} #{state.package_name} #{state.included} => #{version}"
16
17
  false
@@ -0,0 +1,29 @@
1
+ module AptControl::CLI
2
+ class Promote < Climate::Command('promote')
3
+ include Common
4
+ subcommand_of Root
5
+ description "Promote a currently included package from one distribution to another"
6
+
7
+ arg :src_distribution, "Name of distribution to find currently included package version"
8
+ arg :dest_distribution, "Name of distribution to update"
9
+ arg :package, "Name of package to promote"
10
+
11
+ def run
12
+ validate_config!
13
+
14
+ begin
15
+ promote_cmd = AptControl::Commands::Promote.new(
16
+ control_file: control_file,
17
+ package_states: package_states)
18
+
19
+ begin
20
+ promote_cmd.run(arguments[:src_distribution],
21
+ arguments[:dest_distribution],
22
+ arguments[:package])
23
+ rescue ArgumentError => e
24
+ raise Climate::ExitException, e.message
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ module AptControl::CLI
2
+ class Set < Climate::Command('set')
3
+ include Common
4
+ subcommand_of Root
5
+ description "Set a version restriction for a package and distribution"
6
+
7
+ arg :distribution, "Name of distribution"
8
+ arg :package, "Name of package"
9
+ arg :constraint, "Version constraint, i.e. '>= 1.5'"
10
+
11
+ def run
12
+ validate_config!
13
+
14
+ begin
15
+ AptControl::Commands::Set.new(control_file: control_file).
16
+ run(arguments[:distribution], arguments[:package], arguments[:constraint])
17
+ rescue ArgumentError => e
18
+ raise Climate::ExitException, e.message
19
+ end
20
+ end
21
+ end
22
+ end
@@ -11,15 +11,7 @@ module AptControl::CLI
11
11
 
12
12
  if options[:machine_readable]
13
13
  package_states.each do |state|
14
- fields = [
15
- state.dist.name,
16
- state.package_name,
17
- "(#{state.rule.restriction} #{state.rule.version})",
18
- "#{state.includeable? ? 'I' : '.'}#{state.satisfied? ? 'S' : '.'}",
19
- "included=#{state.included || '<none>'}",
20
- "available=#{state.available? ? state.available.join(', ') : '<none>'} "
21
- ]
22
- puts fields.join(' ')
14
+ puts state.status_line
23
15
  end
24
16
  else
25
17
  last_dist = nil
@@ -54,11 +54,18 @@ has the usual set of options for running as an init.d style daemon.
54
54
  at_exit { File.delete(pidfile) if File.exists?(pidfile) } if pidfile
55
55
  end
56
56
 
57
+ # for the watch command, we use the actor version of the apt_site so that
58
+ # reprepro operations are sequential
59
+ def new_include_cmd
60
+ super(apt_site: apt_site.actor)
61
+ end
62
+
57
63
  def start_watching
58
64
  threads = [
59
65
  watch_control_in_new_thread,
60
- watch_build_archive_in_new_thread
61
- ]
66
+ watch_build_archive_in_new_thread,
67
+ jabber_enabled? && start_aptbot_in_new_thread
68
+ ].compact
62
69
 
63
70
  notify("apt_control watcher is up, waiting for changes to control file and new packages...")
64
71
 
@@ -66,6 +73,25 @@ has the usual set of options for running as an init.d style daemon.
66
73
  threads.each(&:join)
67
74
  end
68
75
 
76
+ def start_aptbot_in_new_thread
77
+ Thread.new do
78
+ begin
79
+ bot = AptControl::Bot.new(
80
+ jabber: jabber.actor,
81
+ command_start: jabber.room_nick,
82
+ package_states: package_states,
83
+ include_cmd: new_include_cmd,
84
+ control_file: control_file,
85
+ logger: logger)
86
+
87
+ jabber.add_room_listener(bot.actor)
88
+ rescue => e
89
+ puts "got an error #{e}"
90
+ puts e.backtrace
91
+ end
92
+ end
93
+ end
94
+
69
95
  def watch_control_in_new_thread
70
96
  # update the all the rules if the control file changes
71
97
  Thread.new do
@@ -74,7 +100,7 @@ has the usual set of options for running as an init.d style daemon.
74
100
  notify "Control file reloaded"
75
101
  # FIXME need to do some kind of locking or actor style dev for this
76
102
  # as it looks like there could be some concurrency bugs lurking
77
- includer.perform_for_all(package_states) do |package_state, new_version|
103
+ new_include_cmd.run(package_states) do |package_state, new_version|
78
104
  notify("included package #{package_state.package_name}-#{new_version} in #{package_state.dist.name}")
79
105
  true
80
106
  end
@@ -105,7 +131,7 @@ has the usual set of options for running as an init.d style daemon.
105
131
  updated = matched_states.map do |state|
106
132
  if state.includeable_to.max == new_version
107
133
  begin
108
- includer.perform_for(state, new_version, options[:noop])
134
+ new_include_cmd.perform_for(state, new_version, options[:noop])
109
135
  notify("included package #{package.name}-#{new_version} in #{state.dist.name}")
110
136
  state.dist.name
111
137
  rescue => e
@@ -0,0 +1,6 @@
1
+ module AptControl::Commands
2
+ # general purpose command classes that can be re-used between CLI and jabber bot
3
+ require 'apt_control/commands/set'
4
+ require 'apt_control/commands/include'
5
+ require 'apt_control/commands/promote'
6
+ end
@@ -1,22 +1,20 @@
1
1
  module AptControl
2
+ class Commands::Include
2
3
 
3
- # Wraps the common functionality involved in including the latest includeable
4
- # package in an apt site
5
- class Includer
6
- def initialize(apt_site, build_archive)
7
- @apt_site = apt_site
8
- @build_archive = build_archive
4
+ def initialize(dependencies)
5
+ @apt_site = dependencies.fetch(:apt_site)
6
+ @build_archive = dependencies.fetch(:build_archive)
9
7
  end
10
8
 
11
- def perform_for_all(package_states, &visitor)
12
- package_states.each do |state|
9
+ def run(package_states, &visitor)
10
+ package_states.map do |state|
13
11
  next unless state.includeable?
14
12
 
15
13
  version = state.includeable_to.max
16
14
  perform = (block_given? && yield(state, version)) || true
17
15
 
18
- perform_for(state, version) if perform
19
- end
16
+ perform_for(state, version) && [state, version] if perform
17
+ end.compact
20
18
  end
21
19
 
22
20
  def perform_for(state, version, noop=false)
@@ -0,0 +1,28 @@
1
+ module AptControl
2
+ class Commands::Promote
3
+
4
+ def initialize(dependencies)
5
+ @control_file = dependencies.fetch(:control_file)
6
+ @package_states = dependencies.fetch(:package_states)
7
+ end
8
+
9
+ def run(src_name, dest_name, pkg_name)
10
+ source_dist = @control_file[src_name] or
11
+ raise ArgumentError, "source distribution '#{src_name}' does not exist"
12
+
13
+ dest_dist = @control_file[dest_name] or
14
+ raise ArgumentError, "destination distribution '#{dest_name}' does not exist"
15
+
16
+ src_state = @package_states.find_state(src_name, pkg_name) or
17
+ raise ArgumentError, "package '#{pkg_name}' does not exist in distribution '#{src_name}'"
18
+
19
+ if not src_state.included?
20
+ raise ArgumentError, "no '#{pkg_name}' package included in '#{src_name}' to promote"
21
+ end
22
+ new_constraint = "= #{src_state.included.to_s}"
23
+ dest_dist[pkg_name] = new_constraint
24
+
25
+ @control_file.write
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module AptControl
2
+ class Commands::Set
3
+
4
+ def initialize(dependencies)
5
+ @control_file = dependencies.fetch(:control_file)
6
+ end
7
+
8
+ def run(distribution_name, package_name, constraint)
9
+ distribution = @control_file[distribution_name] or
10
+ raise ArgumentError, "no such distribution: #{distribution_name}"
11
+
12
+ package_rule = distribution[package_name] or
13
+ raise ArgumentError, "no such package: #{package_name}"
14
+
15
+ begin
16
+ package_rule.constraint = constraint
17
+ rescue => e
18
+ raise ArgumentError, "could not set constraint: #{e}"
19
+ end
20
+
21
+ @control_file.write
22
+ end
23
+ end
24
+ end
@@ -4,20 +4,35 @@ module AptControl
4
4
 
5
5
  # Loads and models the contents of a control.ini file
6
6
  # see example-control.ini in root of project for an example
7
+ #
8
+ # TODO some sort of actor wrapper that provides sequential write access to
9
+ # the data model
7
10
  class ControlFile
8
11
 
9
12
  def initialize(path, logger)
10
13
  @logger = logger
11
14
  @watch_mutex = Mutex.new
12
15
  @path = path
13
- inifile = IniFile.load(@path)
14
- @distributions = parse!(inifile)
16
+ @distributions =
17
+ if File.exists?(@path)
18
+ inifile = IniFile.load(@path)
19
+ parse!(inifile)
20
+ else
21
+ []
22
+ end
15
23
  end
16
24
 
17
25
  def distributions
26
+ # not sure that this is strictly necessary - there is no true concurrency
27
+ # in MRI/YARV, but that doesn't mean a thread couldn't be interrupted half
28
+ # way through initialising a variable, does it?
18
29
  @watch_mutex.synchronize { @distributions }
19
30
  end
20
31
 
32
+ def [](dist_name)
33
+ distributions.find {|d| d.name == dist_name }
34
+ end
35
+
21
36
  def dump
22
37
  @watch_mutex.synchronize do
23
38
  @distributions.each do |d|
@@ -29,6 +44,22 @@ module AptControl
29
44
  end
30
45
  end
31
46
 
47
+ def write
48
+ IniFile.new.tap do |inifile|
49
+ inifile.filename = @path
50
+ distributions.each do |distribution|
51
+ inifile[distribution.name] = distribution.inject({}) do |hash, rule|
52
+ hash.tap do |h|
53
+ # quote the restriction, as inifile doesn't do this for you
54
+ # https://github.com/TwP/inifile/pull/16
55
+ h[rule.package_name] = '"' + rule.restriction_string + '"'
56
+ end
57
+ end
58
+ end
59
+ inifile.write
60
+ end
61
+ end
62
+
32
63
  def parse!(inifile)
33
64
  inifile.sections.map do |section|
34
65
  rules = inifile[section].map do |key, value|
@@ -38,6 +69,15 @@ module AptControl
38
69
  end
39
70
  end
40
71
 
72
+ def reload!
73
+ inifile = IniFile.load(@path)
74
+ distributions = parse!(inifile)
75
+
76
+ @watch_mutex.synchronize do
77
+ @distributions = distributions
78
+ end
79
+ end
80
+
41
81
  # Watch the control file for changes, rebuilding
42
82
  # internal data structures when it does
43
83
  def watch(fs_listener_factory, &block)
@@ -48,14 +88,9 @@ module AptControl
48
88
  fs_listener_factory.new(dir, /#{Regexp.quote(fname)}/) do |modified, added, removed|
49
89
  begin
50
90
  @logger.info("Change to control file detected...")
51
- inifile = IniFile.load(path)
52
- distributions = parse!(inifile)
53
-
54
- @watch_mutex.synchronize do
55
- @distributions = distributions
56
- end
57
- @logger.info("...rebuilt")
91
+ reload!
58
92
  yield if block_given?
93
+ @logger.info("...rebuilt")
59
94
  rescue => e
60
95
  @logger.error("Error reloading changes: #{e}")
61
96
  @logger.error(e)
@@ -75,6 +110,10 @@ module AptControl
75
110
 
76
111
  def initialize(name, constraint)
77
112
  @package_name = name
113
+ self.constraint = constraint
114
+ end
115
+
116
+ def constraint=(constraint)
78
117
  version = nil
79
118
  constraint.split(" ").tap do |split|
80
119
  @restriction, version = if split.size == 1
@@ -121,22 +160,47 @@ module AptControl
121
160
  def includeable_to(available)
122
161
  available.select {|a| satisfied_by?(a) }
123
162
  end
163
+
164
+ def restriction_string
165
+ "#{@restriction} #{@version}"
166
+ end
167
+
168
+ # FIXME to_s should include the package name
169
+ alias :to_s :restriction_string
124
170
  end
125
171
 
126
172
  # represents a set of rules mapped to a particular distribution, i.e.
127
173
  # squeeze is a distribution
128
174
  class Distribution
175
+ include Enumerable
176
+
129
177
  def initialize(name, rules)
130
178
  @name = name
131
179
  @package_rules = rules
132
180
  end
181
+
133
182
  attr_reader :name
134
183
  attr_reader :package_rules
135
184
 
185
+ def each(&block)
186
+ (@package_rules || []).each do |rule|
187
+ yield(rule)
188
+ end
189
+ end
190
+
136
191
  # find a PackageRule by package name
137
192
  def [](package_name)
138
193
  package_rules.find {|rule| rule.package_name == package_name }
139
194
  end
195
+
196
+ def []=(package_name, new_constraint)
197
+ existing_rule = self[package_name]
198
+ if existing_rule
199
+ existing_rule.constraint = new_constraint
200
+ else
201
+ package_rules << PackageRule.new(package_name, new_constraint)
202
+ end
203
+ end
140
204
  end
141
205
  end
142
206
  end
@@ -8,15 +8,26 @@ module AptControl
8
8
  include ::Jabber
9
9
 
10
10
  def initialize(options)
11
+ @enabled = options[:enabled]
11
12
  @jid = options[:jid]
12
13
  @password = options[:password]
13
14
  @room_jid = options[:room_jid]
14
15
  @logger = options[:logger]
15
16
 
16
- swallow_errors { connect! } unless options[:defer_connect]
17
+ if @enabled
18
+ @room_nick = @room_jid && @room_jid.split('/').last
19
+ @room_listeners = []
20
+
21
+ swallow_errors { connect! } unless options[:defer_connect]
22
+ end
17
23
  end
18
24
 
25
+ def enabled? ; @enabled ; end
26
+ def room_nick ; @room_nick ; end
27
+
19
28
  def send_message(msg)
29
+ return unless enabled?
30
+
20
31
  if not_connected?
21
32
  connect!
22
33
  end
@@ -52,6 +63,13 @@ module AptControl
52
63
 
53
64
  def not_connected? ; ! connected? ; end
54
65
 
66
+ def add_room_listener(listener)
67
+ @room_listeners << listener
68
+ end
69
+
70
+ include Actors::Proxied
71
+ proxy :send_message
72
+
55
73
  private
56
74
 
57
75
  def attempt_reconnect(&block)
@@ -85,11 +103,36 @@ module AptControl
85
103
  @muc = Jabber::MUC::SimpleMUCClient.new(@client)
86
104
  @muc.join(JID.new(@room_jid))
87
105
  @logger.info("joined room #{@room_jid}")
106
+ setup_muc_callbacks
88
107
  rescue JabberError => e
89
108
  raise ConnectionError.new("error joining room", e)
90
109
  end
91
110
  end
92
111
 
112
+ def setup_muc_callbacks
113
+ @muc.on_message do |time, nick, text|
114
+ next if time # skip history
115
+ next if @room_nick == nick
116
+ notify_room_listeners(text)
117
+ end
118
+ end
119
+
120
+ def notify_room_listeners(text)
121
+ @room_listeners.each do |l|
122
+ begin
123
+ if l.is_a? Celluloid
124
+ l.async.on_message(text)
125
+ else
126
+ l.on_message(text)
127
+ end
128
+ rescue => e
129
+ @logger.error("listener #{l} raised error: #{e}")
130
+ @logger.error(e)
131
+ end
132
+ end
133
+ end
134
+
135
+
93
136
  # Thank you to http://rubyforge.org/projects/nestegg for the pattern
94
137
  class Error < StandardError
95
138
 
@@ -114,4 +157,7 @@ module AptControl
114
157
  class ConnectionError < Error ; end
115
158
  class SendError < Error ; end
116
159
  end
160
+
161
+
162
+
117
163
  end
@@ -17,6 +17,12 @@ module AptControl
17
17
  end
18
18
  end
19
19
  end
20
+
21
+ def find_state(dist_name, package_name)
22
+ find do |state|
23
+ state.dist.name == dist_name && state.package_name == package_name
24
+ end
25
+ end
20
26
  end
21
27
 
22
28
  # Brings together the state of a particular package in a particular
@@ -55,5 +61,17 @@ module AptControl
55
61
  def includeable_to
56
62
  rule.includeable_to(available)
57
63
  end
64
+
65
+ def status_line
66
+ [
67
+ dist.name,
68
+ package_name,
69
+ "(#{rule.restriction} #{rule.version})",
70
+ "#{includeable? ? 'I' : '.'}#{satisfied? ? 'S' : '.'}",
71
+ "included=#{included || '<none>'}",
72
+ "available=#{available? ? available.join(', ') : '<none>'} "
73
+ ].join(' ')
74
+ end
75
+
58
76
  end
59
77
  end
@@ -1,3 +1,3 @@
1
1
  module AptControl
2
- VERSION = '0.4.0'
2
+ VERSION = '1.0.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apt_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-10 00:00:00.000000000 Z
12
+ date: 2013-06-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: popen4
@@ -82,7 +82,7 @@ dependencies:
82
82
  requirements:
83
83
  - - ~>
84
84
  - !ruby/object:Gem::Version
85
- version: '1.1'
85
+ version: '1.2'
86
86
  type: :runtime
87
87
  prerelease: false
88
88
  version_requirements: !ruby/object:Gem::Requirement
@@ -90,7 +90,7 @@ dependencies:
90
90
  requirements:
91
91
  - - ~>
92
92
  - !ruby/object:Gem::Version
93
- version: '1.1'
93
+ version: '1.2'
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: rb-inotify
96
96
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +107,22 @@ dependencies:
107
107
  - - ~>
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0.9'
110
+ - !ruby/object:Gem::Dependency
111
+ name: celluloid
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 0.14.0
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: 0.14.0
110
126
  - !ruby/object:Gem::Dependency
111
127
  name: minitest
112
128
  requirement: !ruby/object:Gem::Requirement
@@ -150,15 +166,22 @@ files:
150
166
  - bin/apt_control
151
167
  - lib/apt_control.rb
152
168
  - lib/apt_control/jabber.rb
169
+ - lib/apt_control/commands.rb
153
170
  - lib/apt_control/package_states.rb
171
+ - lib/apt_control/actors.rb
172
+ - lib/apt_control/bot.rb
154
173
  - lib/apt_control/cli.rb
155
174
  - lib/apt_control/version.rb
156
- - lib/apt_control/includer.rb
157
175
  - lib/apt_control/exec.rb
176
+ - lib/apt_control/cli/set.rb
158
177
  - lib/apt_control/cli/watch.rb
178
+ - lib/apt_control/cli/promote.rb
159
179
  - lib/apt_control/cli/include.rb
160
180
  - lib/apt_control/cli/status.rb
161
181
  - lib/apt_control/build_archive.rb
182
+ - lib/apt_control/commands/set.rb
183
+ - lib/apt_control/commands/promote.rb
184
+ - lib/apt_control/commands/include.rb
162
185
  - lib/apt_control/apt_site.rb
163
186
  - lib/apt_control/control_file.rb
164
187
  homepage: http://github.com/playlouder/apt_control