apt_control 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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