devdnsd 1.7.0 → 2.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.
@@ -1,352 +1,420 @@
1
1
  # encoding: utf-8
2
2
  #
3
- # This file is part of the devdnsd gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
3
+ # This file is part of the devdnsd gem. Copyright (C) 2013 and above Shogun <shogun_panda@me.com>.
4
4
  # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
5
  #
6
6
 
7
7
  # A small DNS server to enable local .dev domain resolution.
8
8
  module DevDNSd
9
- # The main DevDNSd application.
10
- class Application < RExec::Daemon::Base
11
- # Class for ANY DNS request.
12
- ANY_REQUEST = Resolv::DNS::Resource::IN::ANY
9
+ # Methods for the {Application Application} class.
10
+ module ApplicationMethods
11
+ # System management methods.
12
+ module System
13
+ extend ActiveSupport::Concern
14
+
15
+ # Class methods.
16
+ module ClassMethods
17
+ # Returns the name of the daemon.
18
+ #
19
+ # @return [String] The name of the daemon.
20
+ def daemon_name
21
+ File.basename(self.instance.config.pid_file, ".pid")
22
+ end
13
23
 
14
- # List of classes handled in case of DNS request with resource class ANY.
15
- ANY_CLASSES = [Resolv::DNS::Resource::IN::A, Resolv::DNS::Resource::IN::AAAA, Resolv::DNS::Resource::IN::ANY, Resolv::DNS::Resource::IN::CNAME, Resolv::DNS::Resource::IN::HINFO, Resolv::DNS::Resource::IN::MINFO, Resolv::DNS::Resource::IN::MX, Resolv::DNS::Resource::IN::NS, Resolv::DNS::Resource::IN::PTR, Resolv::DNS::Resource::IN::SOA, Resolv::DNS::Resource::IN::TXT]
24
+ # Returns the standard location of the PID file.
25
+ #
26
+ # @return [String] The standard location of the PID file.
27
+ def pid_directory
28
+ File.dirname(self.instance.config.pid_file)
29
+ end
16
30
 
17
- # The {Configuration Configuration} of this application.
18
- attr_reader :config
31
+ # Returns the complete path of the PID file.
32
+ #
33
+ # @return [String] The complete path of the PID file.
34
+ def pid_fn
35
+ self.instance.config.pid_file
36
+ end
37
+ end
19
38
 
20
- # The Mamertes command.
21
- attr_reader :command
39
+ # Gets the path for the resolver file.
40
+ #
41
+ # @param tld [String] The TLD to manage.
42
+ # @return [String] The path for the resolver file.
43
+ def resolver_path(tld = nil)
44
+ tld ||= @config.tld
45
+ "/etc/resolver/#{tld}"
46
+ end
22
47
 
23
- # The logger for this application.
24
- attr_accessor :logger
48
+ # Gets the path for the launch agent file.
49
+ #
50
+ # @param name [String] The base name for the agent.
51
+ # @return [String] The path for the launch agent file.
52
+ def launch_agent_path(name = "it.cowtech.devdnsd")
53
+ ENV["HOME"] + "/Library/LaunchAgents/#{name}.plist"
54
+ end
25
55
 
26
- # Creates a new application.
27
- #
28
- # @param command [Mamertes::Command] The current Mamertes command.
29
- def initialize(command)
30
- @command = command
31
- application = @command.application
56
+ # Executes a shell command.
57
+ #
58
+ # @param command [String] The command to execute.
59
+ # @return [Boolean] `true` if command succeeded, `false` otherwise.
60
+ def execute_command(command)
61
+ system(command)
62
+ end
32
63
 
33
- # Setup logger
34
- Bovem::Logger.start_time = Time.now
35
- @logger = Bovem::Logger.create(Bovem::Logger.get_real_file(application.options["log-file"].value) || Bovem::Logger.default_file, Logger::INFO)
64
+ # Updates DNS cache.
65
+ #
66
+ # @return [Boolean] `true` if command succeeded, `false` otherwise.
67
+ def dns_update
68
+ @logger.info(self.i18n.dns_update)
69
+ self.execute_command("dscacheutil -flushcache")
70
+ end
36
71
 
37
- # Open configuration
38
- begin
39
- overrides = {
40
- :foreground => command.name == "start" ? command.options["foreground"].value : false,
41
- :tld => application.options["tld"].value,
42
- :port => application.options["port"].value,
43
- :pid_file => application.options["pid-file"].value,
44
- :log_file => application.options["log-file"].value,
45
- :log_level => application.options["log-level"].value
46
- }.reject {|k,v| v.nil? }
47
-
48
- @config = DevDNSd::Configuration.new(application.options["configuration"].value, overrides, @logger)
49
-
50
- @logger = nil
51
- @logger = self.get_logger
52
- rescue Bovem::Errors::InvalidConfiguration, DevDNSd::Errors::InvalidRule => e
53
- @logger ? @logger.fatal(e.message) : Bovem::Logger.create("STDERR").fatal("Cannot log to #{config.log_file}. Exiting...")
54
- raise ::SystemExit
72
+ # Checks if we are running on MacOS X.
73
+ #
74
+ # System services are only available on that platform.
75
+ #
76
+ # @return [Boolean] `true` if the current platform is MacOS X, `false` otherwise.
77
+ def is_osx?
78
+ ::RbConfig::CONFIG['host_os'] =~ /^darwin/
55
79
  end
56
80
 
57
- self
58
- end
81
+ # Starts the server in background.
82
+ #
83
+ # @return [Boolean] `true` if action succedeed, `false` otherwise.
84
+ def action_start
85
+ self.get_logger.info(self.i18n.starting)
59
86
 
60
- # Returns the name of the daemon.
61
- #
62
- # @return [String] The name of the daemon.
63
- def self.daemon_name
64
- File.basename(self.instance.config.pid_file, ".pid")
65
- end
87
+ if !Process.respond_to?(:fork) then
88
+ self.logger.warn(self.i18n.no_fork)
89
+ @config.foreground = true
90
+ end
66
91
 
67
- # Returns the standard location of the PID file.
68
- #
69
- # @return [String] The standard location of the PID file.
70
- def self.pid_directory
71
- File.dirname(self.instance.config.pid_file)
72
- end
92
+ @config.foreground ? self.perform_server : RExec::Daemon::Controller.start(self.class)
93
+ true
94
+ end
73
95
 
74
- # Returns the complete path of the PID file.
75
- #
76
- # @return [String] The complete path of the PID file.
77
- def self.pid_fn
78
- self.instance.config.pid_file
79
- end
96
+ # Stops the server in background.
97
+ #
98
+ # @return [Boolean] `true` if action succedeed, `false` otherwise.
99
+ def action_stop
100
+ RExec::Daemon::Controller.stop(self.class)
101
+ true
102
+ end
80
103
 
81
- # Check if we are running on MacOS X.
82
- # System services are only available on that platform.
83
- #
84
- # @return [Boolean] `true` if the current platform is MacOS X, `false` otherwise.
85
- def is_osx?
86
- ::Config::CONFIG['host_os'] =~ /^darwin/
87
- end
104
+ # Installs the application into the autolaunch.
105
+ #
106
+ # @return [Boolean] `true` if action succedeed, `false` otherwise.
107
+ def action_install
108
+ manage_installation(self.launch_agent_path, self.resolver_path, :create_resolver, :create_agent, :load_agent)
109
+ end
88
110
 
89
- # Gets the current logger of the application.
90
- #
91
- # @return [Logger] The current logger of the application.
92
- def get_logger
93
- @logger ||= Bovem::Logger.create(@config.foreground ? Bovem::Logger.default_file : @config.log_file, @config.log_level, @log_formatter)
94
- end
111
+ # Uninstalls the application from the autolaunch.
112
+ #
113
+ # @return [Boolean] `true` if action succedeed, `false` otherwise.
114
+ def action_uninstall
115
+ manage_installation(self.launch_agent_path, self.resolver_path, :delete_resolver, :unload_agent, :delete_agent)
116
+ end
95
117
 
96
- # Gets the path for the resolver file.
97
- #
98
- # @param tld [String] The TLD to manage.
99
- # @return [String] The path for the resolver file.
100
- def resolver_path(tld = nil)
101
- tld ||= @config.tld
102
- "/etc/resolver/#{tld}"
103
- end
118
+ private
119
+ # Manages a OSX agent.
120
+ #
121
+ # @param launch_agent [String] The agent path.
122
+ # @param resolver_path [String] The resolver path.
123
+ # @param first_operation [Symbol] The first operation to execute.
124
+ # @param second_operation [Symbol] The second operation to execute.
125
+ # @param third_operation [Symbol] The third operation to execute.
126
+ # @return [Boolean] `true` if operation succedeed, `false` otherwise.
127
+ def manage_installation(launch_agent, resolver_path, first_operation, second_operation, third_operation)
128
+ rv = true
129
+
130
+ rv = check_agent_available
131
+ rv = send(first_operation, launch_agent, resolver_path) if rv
132
+ rv = send(second_operation, launch_agent, resolver_path) if rv
133
+ rv = send(third_operation, launch_agent, resolver_path) if rv
134
+ self.dns_update
135
+ rv
136
+ end
104
137
 
105
- # Gets the path for the launch agent file.
106
- #
107
- # @param name [String] The base name for the agent.
108
- # @return [String] The path for the launch agent file.
109
- def launch_agent_path(name = "it.cowtech.devdnsd")
110
- ENV["HOME"] + "/Library/LaunchAgents/#{name}.plist"
111
- end
138
+ # Deletes a file
139
+ #
140
+ # @param file [String] The file to delete.
141
+ # @param before_message [Symbol] The message to show before deleting.
142
+ # @param error_message [Symbol] The message to show in case of errors.
143
+ # @return [Boolean] `true` if the file have been deleted, `false` otherwise.
144
+ def delete_file(file, before_message, error_message)
145
+ begin
146
+ self.logger.info(self.i18n.send(before_message, file))
147
+ ::File.delete(file)
148
+ true
149
+ rescue => e
150
+ self.logger.warn(self.i18n.send(error_message))
151
+ false
152
+ end
153
+ end
112
154
 
113
- # Executes a shell command.
114
- #
115
- # @param command [String] The command to execute.
116
- # @return [Boolean] `true` if command succeeded, `false` otherwise.
117
- def execute_command(command)
118
- system(command)
119
- end
155
+ # Checks if agent is enabled (that is, we are on OSX).
156
+ #
157
+ # @return [Boolean] `true` if the agent is enabled, `false` otherwise.
158
+ def check_agent_available
159
+ rv = true
160
+ if !self.is_osx? then
161
+ logger.fatal(self.i18n.no_agent)
162
+ rv = false
163
+ end
120
164
 
121
- # Updates DNS cache.
122
- #
123
- # @return [Boolean] `true` if command succeeded, `false` otherwise.
124
- def dns_update
125
- @logger.info("Flushing DNS cache and resolvers ...")
126
- self.execute_command("dscacheutil -flushcache")
127
- end
165
+ rv
166
+ end
128
167
 
129
- # Starts the DNS server.
130
- #
131
- # @return [Object] The result of stop callbacks.
132
- def perform_server
133
- RubyDNS::run_server(:listen => [[:udp, @config.address, @config.port.to_integer]]) do
134
- self.logger = DevDNSd::Application.instance.logger
135
-
136
- match(/.+/, DevDNSd::Application::ANY_CLASSES) do |match_data, transaction|
137
- transaction.append_question!
138
-
139
- DevDNSd::Application.instance.config.rules.each do |rule|
140
- begin
141
- # Get the subset of handled class that is valid for the rule
142
- resource_classes = DevDNSd::Application::ANY_CLASSES & rule.resource_class.ensure_array
143
- resource_classes = resource_classes & [transaction.resource_class] if transaction.resource_class != DevDNSd::Application::ANY_REQUEST
144
-
145
- if resource_classes.present? then
146
- resource_classes.each do |resource_class| # Now for every class
147
- matches = rule.match_host(match_data[0])
148
- DevDNSd::Application.instance.process_rule(rule, resource_class, rule.is_regexp? ? matches : nil, transaction) if matches
149
- end
150
- end
151
- rescue ::Exception => e
152
- raise e
153
- end
168
+ # Creates a OSX resolver.
169
+ #
170
+ # @param launch_agent [String] The agent path.
171
+ # @param resolver_path [String] The resolver path.
172
+ # @return [Boolean] `true` if operation succedeed, `false` otherwise.
173
+ def create_resolver(launch_agent, resolver_path)
174
+ begin
175
+ self.logger.info(self.i18n.resolver_creating(resolver_path))
176
+ write_resolver(resolver_path)
177
+ true
178
+ rescue
179
+ self.logger.error(self.i18n.resolver_creating_error)
180
+ false
154
181
  end
155
182
  end
156
183
 
157
- # Default DNS handler
158
- otherwise do |transaction|
159
- transaction.failure!(:NXDomain)
184
+ # Writes a OSX resolver.
185
+ #
186
+ # @param resolver_path [String] The resolver path.
187
+ def write_resolver(resolver_path)
188
+ ::File.open(resolver_path, "w") {|f|
189
+ f.write("nameserver 127.0.0.1\n")
190
+ f.write("port #{@config.port}")
191
+ f.flush
192
+ }
160
193
  end
161
194
 
162
- # Attach event handlers
163
- self.on(:start) do
164
- DevDNSd::Application.instance.on_start
195
+ # Deletes a OSX resolver.
196
+ #
197
+ # @param launch_agent [String] The agent path.
198
+ # @param resolver_path [String] The resolver path.
199
+ # @return [Boolean] `true` if operation succedeed, `false` otherwise.
200
+ def delete_resolver(launch_agent, resolver_path)
201
+ delete_file(resolver_path, :resolver_deleting, :resolver_deleting_error)
165
202
  end
166
203
 
167
- self.on(:stop) do
168
- DevDNSd::Application.instance.on_stop
204
+ # Creates a OSX system agent.
205
+ #
206
+ # @param launch_agent [String] The agent path.
207
+ # @param resolver_path [String] The resolver path.
208
+ # @return [Boolean] `true` if operation succedeed, `false` otherwise.
209
+ def create_agent(launch_agent, resolver_path)
210
+ begin
211
+ self.logger.info(self.i18n.agent_creating(launch_agent))
212
+ write_agent(launch_agent)
213
+ self.execute_command("plutil -convert binary1 \"#{launch_agent}\"")
214
+ true
215
+ rescue
216
+ self.logger.error(self.i18n.agent_creating_error)
217
+ false
218
+ end
169
219
  end
170
- end
171
- end
172
220
 
173
- # Processes a DNS rule.
174
- #
175
- # @param rule [Rule] The rule to process.
176
- # @param type [Class] The type of request.
177
- # @param match_data [MatchData|nil] If the rule pattern was a Regexp, then this holds the match data, otherwise `nil` is passed.
178
- # @param transaction [Transaction] The current DNS transaction (http://rubydoc.info/gems/rubydns/RubyDNS/Transaction).
179
- # @return A reply for the request if matched, otherwise `false` or `nil`.
180
- def process_rule(rule, type, match_data, transaction)
181
- is_regex = rule.match.is_a?(::Regexp)
182
- type = DevDNSd::Rule.resource_class_to_symbol(type)
183
-
184
- DevDNSd::Application.instance.logger.debug("Found match on #{rule.match} with type #{type}.")
185
-
186
- if !rule.block.nil? then
187
- reply = rule.block.call(match_data, type, transaction)
188
- else
189
- reply = rule.reply
190
- end
191
-
192
- if is_regex && reply && match_data[0] then
193
- reply = match_data[0].gsub(rule.match, reply.gsub("$", "\\"))
194
- end
195
221
 
196
- DevDNSd::Application.instance.logger.debug(reply ? "Reply is #{reply} with type #{type}." : "No reply found.")
197
-
198
- if reply then
199
- options = rule.options
200
-
201
- final_reply = []
222
+ # Writes a OSX system agent.
223
+ #
224
+ # @param launch_agent [String] The agent path.
225
+ def write_agent(launch_agent)
226
+ ::File.open(launch_agent, "w") {|f|
227
+ f.write({"KeepAlive" => true, "Label" => "it.cowtech.devdnsd", "Program" => (::Pathname.new(Dir.pwd) + $0).to_s, "ProgramArguments" => ($ARGV ? $ARGV[0, $ARGV.length - 1] : []), "RunAtLoad" => true}.to_json)
228
+ f.flush
229
+ }
230
+ end
202
231
 
203
- case type
204
- when :MX
205
- preference = options.delete(:preference)
206
- preference = preference.nil? ? 10 : preference.to_integer(10)
207
- final_reply << preference
232
+ # Deletes a OSX system agent.
233
+ #
234
+ # @param launch_agent [String] The agent path.
235
+ # @param resolver_path [String] The resolver path.
236
+ # @return [Boolean] `true` if operation succedeed, `false` otherwise.
237
+ def delete_agent(launch_agent, resolver_path)
238
+ delete_file(launch_agent, :agent_deleting, :agent_deleting_error)
208
239
  end
209
240
 
210
- if [:A, :AAAA].include?(type) then
211
- final_reply << reply
212
- else
213
- final_reply << Resolv::DNS::Name.create(reply)
241
+ # Loads a OSX system agent.
242
+ #
243
+ # @param launch_agent [String] The agent path.
244
+ # @param resolver_path [String] The resolver path.
245
+ # @return [Boolean] `true` if operation succedeed, `false` otherwise.
246
+ def load_agent(launch_agent, resolver_path)
247
+ begin
248
+ self.logger.info(self.i18n.agent_loading(launch_agent))
249
+ self.execute_command("launchctl load -w \"#{launch_agent}\" > /dev/null 2>&1")
250
+ true
251
+ rescue
252
+ self.logger.error(self.i18n.agent_loading_error)
253
+ false
254
+ end
214
255
  end
215
256
 
216
- final_reply << options.merge({:resource_class => DevDNSd::Rule.symbol_to_resource_class(type)})
217
- transaction.respond!(*final_reply)
218
- elsif reply == false then
219
- false
220
- else
221
- reply
222
- end
257
+ # Unoads a OSX system agent.
258
+ #
259
+ # @param launch_agent [String] The agent path.
260
+ # @param resolver_path [String] The resolver path.
261
+ # @return [Boolean] `true` if operation succedeed, `false` otherwise.
262
+ def unload_agent(launch_agent, resolver_path)
263
+ begin
264
+ self.logger.info(self.i18n.agent_unloading(launch_agent))
265
+ self.execute_command("launchctl unload -w \"#{launch_agent}\" > /dev/null 2>&1")
266
+ true
267
+ rescue => e
268
+ self.logger.warn(self.i18n.agent_unloading_error)
269
+ false
270
+ end
271
+ end
223
272
  end
224
273
 
225
- # Starts the server in background.
226
- #
227
- # @return [Boolean] `true` if action succedeed, `false` otherwise.
228
- def action_start
229
- logger = self.get_logger
230
-
231
- logger.info("Starting DevDNSd ...")
274
+ # Methods to process requests.
275
+ module Server
276
+ # Starts the DNS server.
277
+ #
278
+ # @return [Object] The result of stop callbacks.
279
+ def perform_server
280
+ application = self
281
+ RubyDNS::run_server(listen: [[:udp, @config.address, @config.port.to_integer]]) do
282
+ self.logger = application.logger
283
+
284
+ match(/.+/, DevDNSd::Application::ANY_CLASSES) do |match_data, transaction|
285
+ transaction.append_question!
286
+ application.config.rules.each { |rule| application.process_rule_in_classes(rule, match_data, transaction) } # During debugging, wrap the inside of the block with a begin rescue and PRINT the exception because RubyDNS hides it.
287
+ end
232
288
 
233
- if @config.foreground then
234
- self.perform_server
235
- else
236
- RExec::Daemon::Controller.start(self.class)
289
+ # Default DNS handler and event handlers
290
+ otherwise { |transaction| transaction.failure!(:NXDomain) }
291
+ self.on(:start) { application.on_start }
292
+ self.on(:stop) { application.on_stop }
293
+ end
237
294
  end
238
295
 
239
- true
240
- end
241
-
242
- # Stops the server in background.
243
- #
244
- # @return [Boolean] `true` if action succedeed, `false` otherwise.
245
- def action_stop
246
- RExec::Daemon::Controller.stop(self.class)
247
-
248
- true
249
- end
250
-
251
- # Installs the server into the system.
252
- #
253
- # @return [Boolean] `true` if action succedeed, `false` otherwise.
254
- def action_install
255
- logger = get_logger
256
-
257
- if !self.is_osx? then
258
- logger.fatal("Install DevDNSd as a local resolver is only available on MacOSX.")
259
- return false
296
+ # Processes a DNS rule.
297
+ #
298
+ # @param rule [Rule] The rule to process.
299
+ # @param type [Symbol] The type of the query.
300
+ # @param match_data [MatchData|nil] If the rule pattern was a Regexp, then this holds the match data, otherwise `nil` is passed.
301
+ # @param transaction [RubyDNS::Transaction] The current DNS transaction (http://rubydoc.info/gems/rubydns/RubyDNS/Transaction).
302
+ def process_rule(rule, type, match_data, transaction)
303
+ reply, type = perform_process_rule(rule, type, match_data, transaction)
304
+ self.logger.debug(reply ? self.i18n.reply(reply, type) : self.i18n.no_reply)
305
+
306
+ if reply then
307
+ transaction.respond!(*finalize_reply(reply, rule, type))
308
+ elsif reply == false then
309
+ false
310
+ else
311
+ nil
312
+ end
260
313
  end
261
314
 
262
- resolver_file = self.resolver_path
263
- launch_agent = self.launch_agent_path
264
-
265
- # Installs the resolver
266
- begin
267
- logger.info("Installing the resolver in #{resolver_file} ...")
268
-
269
- open(resolver_file, "w") {|f|
270
- f.write("nameserver 127.0.0.1\n")
271
- f.write("port #{@config.port}")
272
- f.flush
273
- }
274
- rescue => e
275
- logger.error("Cannot create the resolver file.")
276
- return false
315
+ # Processes a rule against a set of DNS resource classes.
316
+ #
317
+ # @param rule [Rule] The rule to process.
318
+ # @param match_data [MatchData|nil] If the rule pattern was a Regexp, then this holds the match data, otherwise `nil` is passed.
319
+ # @param transaction [RubyDNS::Transaction] The current DNS transaction (http://rubydoc.info/gems/rubydns/RubyDNS/Transaction).
320
+ def process_rule_in_classes(rule, match_data, transaction)
321
+ # Get the subset of handled class that is valid for the rule
322
+ resource_classes = DevDNSd::Application::ANY_CLASSES & rule.resource_class.ensure_array
323
+ resource_classes = resource_classes & [transaction.resource_class] if transaction.resource_class != DevDNSd::Application::ANY_REQUEST
324
+
325
+ if resource_classes.present? then
326
+ resource_classes.each do |resource_class| # Now for every class
327
+ matches = rule.match_host(match_data[0])
328
+ self.process_rule(rule, resource_class, rule.is_regexp? ? matches : nil, transaction) if matches
329
+ end
330
+ end
277
331
  end
278
332
 
279
- begin
280
- logger.info("Creating the launch agent in #{launch_agent} ...")
333
+ private
334
+ # Performs the processing of a rule.
335
+ #
336
+ # @param rule [Rule] The rule to process.
337
+ # @param type [Symbol] The type of query.
338
+ # @param match_data [MatchData|nil] If the rule pattern was a Regexp, then this holds the match data, otherwise `nil` is passed.
339
+ # @param transaction [RubyDNS::Transaction] The current DNS transaction (http://rubydoc.info/gems/rubydns/RubyDNS/Transaction).
340
+ # @return [Array] The type and reply to the query.
341
+ def perform_process_rule(rule, type, match_data, transaction)
342
+ type = DevDNSd::Rule.resource_class_to_symbol(type)
343
+ reply = !rule.block.nil? ? rule.block.call(match_data, type, transaction) : rule.reply
344
+ reply = match_data[0].gsub(rule.match, reply.gsub("$", "\\")) if rule.match.is_a?(::Regexp) && reply && match_data[0]
345
+
346
+ self.logger.debug(self.i18n.match(rule.match, type))
347
+ [reply, type]
348
+ end
281
349
 
282
- args = $ARGV ? $ARGV[0, $ARGV.length - 1] : []
350
+ # Finalizes a query to return to the client.
351
+ #
352
+ # @param reply [String] The reply to send to the client.
353
+ # @param rule [Rule] The rule to process.
354
+ # @param type [Symbol] The type of query.
355
+ def finalize_reply(reply, rule, type)
356
+ rv = []
357
+ rv << rule.options.delete(:preference).to_integer(10) if type == :MX
358
+ rv << ([:A, :AAAA].include?(type) ? reply : Resolv::DNS::Name.create(reply))
359
+ rv << rule.options.merge({resource_class: DevDNSd::Rule.symbol_to_resource_class(type, @locale)})
360
+ rv
361
+ end
362
+ end
363
+ end
283
364
 
284
- plist = {"KeepAlive" => true, "Label" => "it.cowtech.devdnsd", "Program" => (::Pathname.new(Dir.pwd) + $0).to_s, "ProgramArguments" => args, "RunAtLoad" => true}
285
- ::File.open(launch_agent, "w") {|f|
286
- f.write(plist.to_json)
287
- f.flush
288
- }
289
- self.execute_command("plutil -convert binary1 \"#{launch_agent}\"")
290
- rescue => e
291
- logger.error("Cannot create the launch agent.")
292
- return false
293
- end
365
+ # The main DevDNSd application.
366
+ #
367
+ # @attribute [r] config
368
+ # @return [Configuration] The {Configuration Configuration} of this application.
369
+ # @attribute [r] command
370
+ # @return [Mamertes::Command] The Mamertes command.
371
+ # @attribute logger
372
+ # @return [Bovem::Logger] The logger for this application.
373
+ # @attribute [r] locale
374
+ # @return [Symbol|nil] The current application locale.
375
+ class Application < RExec::Daemon::Base
376
+ # Class for ANY DNS request.
377
+ ANY_REQUEST = Resolv::DNS::Resource::IN::ANY
294
378
 
295
- begin
296
- logger.info("Loading the launch agent ...")
297
- self.execute_command("launchctl load -w \"#{launch_agent}\" > /dev/null 2>&1")
298
- rescue => e
299
- logger.error("Cannot load the launch agent.")
300
- return false
301
- end
379
+ # List of classes handled in case of DNS request with resource class ANY.
380
+ ANY_CLASSES = [Resolv::DNS::Resource::IN::A, Resolv::DNS::Resource::IN::AAAA, Resolv::DNS::Resource::IN::ANY, Resolv::DNS::Resource::IN::CNAME, Resolv::DNS::Resource::IN::HINFO, Resolv::DNS::Resource::IN::MINFO, Resolv::DNS::Resource::IN::MX, Resolv::DNS::Resource::IN::NS, Resolv::DNS::Resource::IN::PTR, Resolv::DNS::Resource::IN::SOA, Resolv::DNS::Resource::IN::TXT]
302
381
 
303
- self.dns_update
382
+ include Lazier::I18n
383
+ include DevDNSd::ApplicationMethods::System
384
+ include DevDNSd::ApplicationMethods::Server
304
385
 
305
- true
306
- end
386
+ attr_reader :config
387
+ attr_reader :command
388
+ attr_accessor :logger
389
+ attr_reader :locale
307
390
 
308
- # Uninstalls the server from the system.
391
+ # Creates a new application.
309
392
  #
310
- # @return [Boolean] `true` if action succedeed, `false` otherwise.
311
- def action_uninstall
312
- logger = self.get_logger
313
-
314
- if !self.is_osx? then
315
- logger.fatal("Install DevDNSd as a local resolver is only available on MacOSX.")
316
- return false
317
- end
318
-
319
- resolver_file = self.resolver_path
320
- launch_agent = self.launch_agent_path
393
+ # @param command [Mamertes::Command] The current Mamertes command.
394
+ # @param locale [Symbol] The locale to use for the application.
395
+ def initialize(command, locale)
396
+ self.i18n_setup(:devdnsd, ::File.absolute_path(::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/"))
397
+ self.i18n = locale
321
398
 
322
- # Remove the resolver
323
- begin
324
- logger.info("Deleting the resolver #{resolver_file} ...")
325
- ::File.delete(resolver_file)
326
- rescue => e
327
- logger.warn("Cannot delete the resolver file.")
328
- return false
329
- end
399
+ @locale = locale
400
+ @command = command
401
+ options = @command.application.get_options.reject {|k,v| v.nil? }
330
402
 
331
- # Unload the launch agent.
332
- begin
333
- self.execute_command("launchctl unload -w \"#{launch_agent}\" > /dev/null 2>&1")
334
- rescue => e
335
- logger.warn("Cannot unload the launch agent.")
336
- end
403
+ # Setup logger
404
+ Bovem::Logger.start_time = Time.now
405
+ @logger = Bovem::Logger.create(Bovem::Logger.get_real_file(options["log_file"]) || Bovem::Logger.default_file, Logger::INFO)
337
406
 
338
- # Delete the launch agent.
339
- begin
340
- logger.info("Deleting the launch agent #{launch_agent} ...")
341
- ::File.delete(launch_agent)
342
- rescue => e
343
- logger.warn("Cannot delete the launch agent.")
344
- return false
345
- end
407
+ # Open configuration
408
+ read_configuration(options)
346
409
 
347
- self.dns_update
410
+ self
411
+ end
348
412
 
349
- true
413
+ # Gets the current logger of the application.
414
+ #
415
+ # @return [Logger] The current logger of the application.
416
+ def get_logger
417
+ @logger ||= Bovem::Logger.create(@config.foreground ? Bovem::Logger.default_file : @config.log_file, @config.log_level, @log_formatter)
350
418
  end
351
419
 
352
420
  # This method is called when the server starts. By default is a no-op.
@@ -364,11 +432,12 @@ module DevDNSd
364
432
  # Returns a unique (singleton) instance of the application.
365
433
  #
366
434
  # @param command [Mamertes::Command] The current Mamertes command.
435
+ # @param locale [Symbol] The locale to use for the application.
367
436
  # @param force [Boolean] If to force recreation of the instance.
368
437
  # @return [Application] The unique (singleton) instance of the application.
369
- def self.instance(command = nil, force = false)
438
+ def self.instance(command = nil, locale = nil, force = false)
370
439
  @instance = nil if force
371
- @instance ||= DevDNSd::Application.new(command) if command
440
+ @instance ||= DevDNSd::Application.new(command, locale) if command
372
441
  @instance
373
442
  end
374
443
 
@@ -381,7 +450,30 @@ module DevDNSd
381
450
 
382
451
  # Stops the application.
383
452
  def self.quit
384
- ::EventMachine.stop
453
+ ::EventMachine.stop rescue nil
454
+ end
455
+
456
+ # Check if the current implementation supports DevDNSd.
457
+ def self.check_ruby_implementation
458
+ if defined?(Rubinius) || defined?(JRuby) then
459
+ Kernel.puts(Lazier::Localizer.new(:devdnsd, ::File.absolute_path(::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/")).i18n.no_jruby_rubinius)
460
+ Kernel.exit(0)
461
+ end
385
462
  end
463
+
464
+ private
465
+ # Reads configuration.
466
+ #
467
+ # @param options [Hash] The configuration to read.
468
+ def read_configuration(options)
469
+ begin
470
+ @config = DevDNSd::Configuration.new(options["configuration"], options, @logger)
471
+ @logger = nil
472
+ @logger = self.get_logger
473
+ rescue Bovem::Errors::InvalidConfiguration => e
474
+ @logger ? @logger.fatal(e.message) : Bovem::Logger.create("STDERR").fatal(self.i18n.logging_failed(log_file))
475
+ raise ::SystemExit
476
+ end
477
+ end
386
478
  end
387
479
  end