Merge branch 'maintenance-webyast-1.0' into tmp/lslezak/bnc_607684
[opensuse:yast-rest-service.git] / plugins / patches / app / models / resolvable.rb
1 #
2 # Model for resolvables available via package kit
3 #
4 require "dbus"
5 require 'socket'
6 require 'thread'
7
8 require 'exceptions'
9
10 class Resolvable
11
12   attr_accessor   :resolvable_id,
13                   :kind,
14                   :name,
15                   :version,
16                   :arch,
17                   :repo,
18                   :summary,
19                   :installing
20
21 private
22
23   # allow only one thread accessing PackageKit
24   @@package_kit_mutex = Mutex.new
25
26   #
27   # Resolvable.packagekit_connect
28   #
29   # connect to PackageKit and create Transaction proxy
30   #
31   # return Array of <transaction proxy>,<packagekit interface>,<transaction 
32   #
33   # Reference: http://www.packagekit.org/gtk-doc/index.html
34   #
35
36   def self.packagekit_connect
37     system_bus = DBus::SystemBus.instance
38     # connect to PackageKit service via SystemBus
39     pk_service = system_bus.service("org.freedesktop.PackageKit")
40     
41     # Create PackageKit proxy object
42     packagekit_proxy = pk_service.object("/org/freedesktop/PackageKit")
43
44     # learn about object
45     packagekit_proxy.introspect
46     
47     # use the (generic) 'PackageKit' interface
48     packagekit_iface = packagekit_proxy["org.freedesktop.PackageKit"]
49     
50     # get transaction id via this interface
51     tid = packagekit_iface.GetTid
52     
53     # retrieve transaction (proxy) object
54     transaction_proxy = pk_service.object(tid[0])
55     transaction_proxy.introspect
56     
57     # use the 'Transaction' interface
58     transaction_iface = transaction_proxy["org.freedesktop.PackageKit.Transaction"]
59     transaction_proxy.default_iface = "org.freedesktop.PackageKit.Transaction"
60
61     [transaction_iface, packagekit_iface]
62   end
63
64 public
65
66   # default constructor
67   def initialize(attributes)
68     attributes.each do |key, value|
69       instance_variable_set("@#{key}", value)
70     end
71   end
72
73   def id
74     @resolvable_id
75   end
76
77   def id=(id_val)
78     @resolvable_id = id_val
79   end
80
81   # get xml representation of instance
82   # tag: name of toplevel tag (i.e. :package)
83   #
84   def to_xml( tag, options = {} )
85     xml = options[:builder] ||= Builder::XmlMarkup.new(options)
86     xml.instruct! unless options[:skip_instruct]
87
88     xml.tag! tag do
89       xml.tag!(:resolvable_id, @resolvable_id, {:type => "integer"} )
90       xml.tag!(:kind, @kind )
91       xml.tag!(:name, @name )
92       xml.tag!(:version, @version )
93       xml.tag!(:arch, @arch )
94       xml.tag!(:repo, @repo )
95       xml.tag!(:summary, @summary )
96       xml.tag!(:installing, @installing, {:type => "boolean"} )
97     end
98
99   end
100   
101   def to_json( options = {} )
102     hash = Hash.from_xml(self.to_xml())
103     return hash.to_json
104   end
105
106   # returns the modification time of the resolvable
107   # which you can use for cache policy purposes
108   def self.mtime
109     # we look for the most recent (max) modification time
110     # of either the package database or libzypp cache files
111     [ File.stat("/var/lib/rpm/Packages").mtime,
112       File.stat("/var/cache/zypp/solv").mtime,
113       * Dir["/var/cache/zypp/solv/*/solv"].map{ |x| File.stat(x).mtime } ].max
114   end
115
116   #
117   # Execute PackageKit transaction method
118   #
119   # method: method to execute
120   # args: arguments to method
121   # signal: signal to intercept (usuallay "Package")
122   # block: block to run on signal
123   #
124   def self.execute(method, args, signal, &block)
125     begin
126       dbusloop = DBus::Main.new
127       transaction_iface, packagekit_iface = self.packagekit_connect
128     
129       proxy = transaction_iface.object
130     
131       proxy.on_signal(signal.to_s, &block)
132       proxy.on_signal("ErrorCode") {|u1,u2| dbusloop.quit }
133       proxy.on_signal("Finished") {|u1,u2| dbusloop.quit }
134       # Do the call only when all signal handlers are in place,
135       # otherwise Finished can arrive early and dbusloop will never
136       # quit, bnc#561578
137       transaction_iface.send(method.to_sym, *args)
138
139       dbusloop << DBus::SystemBus.instance
140       dbusloop.run
141
142       packagekit_iface.SuggestDaemonQuit
143     rescue DBus::Error => dbus_error
144       # check if it is a known error
145       raise ServiceNotAvailable.new('PackageKit') if dbus_error.message =~ /org.freedesktop.DBus.Error.ServiceUnknown/
146       # otherwise rethrow
147       raise dbus_error
148     rescue Exception => e
149       raise e
150     end
151   end
152
153   # create a unique ID for BackgroundManager
154   def self.bgid(what)
155     "packagekit_install_#{what}"
156   end
157
158   # install an update, based on the PackageKit
159   # id ("<name>;<id>;<arch>;<repo>")
160   #
161   def self.package_kit_install(pk_id, background = false)
162     Rails.logger.debug "Installing #{pk_id}, background: #{background.inspect}"
163
164     # background process doesn't work correctly if class reloading is active
165     # (static class members are lost between requests)
166     if background && !bm.background_enabled?
167       Rails.logger.info "Class reloading is active, cannot use background thread (set config.cache_classes = true)"
168       background = false
169     end
170     Rails.logger.debug "Background: #{background.inspect}"
171
172     if background
173       proc_id = bgid(pk_id)
174       if bm.process_finished? proc_id
175         Rails.logger.debug "Patch installation request #{proc_id} is done"
176         ret = bm.get_value proc_id
177
178         # check for exception
179         if ret.is_a? StandardError
180           raise ret
181         end
182
183         return ret
184       end
185
186       running = bm.get_progress proc_id
187       if running
188         Rails.logger.debug "Request #{proc_id} is already running: #{running.inspect}"
189         return running
190       end
191
192       bm.add_process proc_id
193
194       Rails.logger.info "Starting background thread for installing patches..."
195       # run the patch query in a separate thread
196       Thread.new do
197         @@package_kit_mutex.synchronize do
198           res = subprocess_install pk_id
199
200           # check for exception
201           unless res.is_a? StandardError
202             Rails.logger.info "*** Patch install thread: Result: #{res.inspect}"
203           else
204             Rails.logger.debug "*** Patch install thread: Exception raised: #{res.inspect}"
205           end
206           bm.finish_process(proc_id, res)
207         end
208       end
209
210       return bm.get_progress(proc_id)
211     else
212       return do_package_kit_install(pk_id)
213     end
214   end
215
216   def self.do_package_kit_install(pk_id, bs = nil)
217     ok = true
218     transaction_iface, packagekit_iface = self.packagekit_connect
219
220     proxy = transaction_iface.object
221     proxy.on_signal("Package") do |line1,line2,line3|
222       Rails.logger.debug "  update package: #{line2}"
223       if bs
224         bs.status = "#{line1} #{line2}"
225       end
226     end
227
228     $stderr.puts "do_package_kit_install: installing #{pk_id}"
229
230     dbusloop = DBus::Main.new
231     dbusloop << DBus::SystemBus.instance
232
233     proxy.on_signal("Finished") {|u1,u2| dbusloop.quit }
234     proxy.on_signal("ErrorCode") do |u1,u2|
235       ok = false
236       dbusloop.quit
237     end
238     transaction_iface.UpdatePackages(true, [pk_id]) # FIXME revert back !!!!!
239
240     dbusloop.run
241     packagekit_iface.SuggestDaemonQuit
242
243     return ok
244   end
245
246   # installs this
247   def install
248     self.class.install(id)
249   end
250
251   # Patch.install(patch)
252   # Patch.install(id)
253   def self.install(patch)
254     if patch.is_a?(Patch)
255       update_id = "#{patch.name};#{patch.resolvable_id};#{patch.arch};#{@patch.repo}"
256       Rails.logger.debug "Install Update: #{update_id}"
257       self.package_kit_install(update_id)
258     else
259       # if is not an object, assume it is an id
260       patch_id = patch
261       patch = Patch.find(patch_id)
262       raise "Can't install update #{patch_id} because it does not exist" if patch.nil? or not patch.is_a?(Patch)
263       self.install(patch)
264     end
265   end
266
267
268   private
269
270   def self.subprocess_script
271     # find the helper script
272     script = File.join(RAILS_ROOT, 'vendor/plugins/patches/scripts/install_patches.rb')
273
274     unless File.exists? script
275       script = File.join(RAILS_ROOT, '../plugins/patches/scripts/install_patches.rb')
276
277       unless File.exists? script
278         raise 'File patches/scripts/install_patches.rb was not found!'
279       end
280     end
281
282     Rails.logger.debug "Using #{script} script file"
283     script
284   end
285
286   def self.subprocess_command(what)
287     raise "Invalid parameter" if what.to_s.include?("'") or what.to_s.include?('\\')
288     "cd #{RAILS_ROOT} && #{File.join(RAILS_ROOT, 'script/runner')} -e #{ENV['RAILS_ENV'] || 'development'} #{subprocess_script} '#{what}'"
289   end
290
291   # IO functions moved to separate methods for easy mocking/testing
292
293   def self.open_subprocess(what)
294     IO.popen(subprocess_command what)
295   end
296
297   def self.read_subprocess(subproc)
298     subproc.readline
299   end
300
301   def self.eof_subprocess?(subproc)
302     subproc.eof?
303   end
304
305
306   def self.subprocess_install(what)
307     # open subprocess
308     subproc = open_subprocess what
309
310     result = nil
311
312     while !eof_subprocess?(subproc) do
313       begin
314         line = read_subprocess subproc
315
316         unless line.blank?
317           received = Hash.from_xml(line)
318
319           # is it a progress or the final list?
320           if received.has_key? 'patch_installation'
321             Rails.logger.debug "Received background patch installation result: #{received['patch_installation'].inspect}"
322             # create Patch objects
323             result = received['patch_installation']['result']
324           elsif received.has_key? 'background_status'
325             s = received['background_status']
326
327             bm.update_progress bgid(what) do |bs|
328               bs.status = s['status']
329               bs.progress = s['progress']
330               bs.subprogress = s['subprogress']
331             end
332           elsif received.has_key? 'error'
333             return PackageKitError.new(received['error']['description'])
334           else
335             Rails.logger.warn "*** Patch installtion thread: Received unknown input: #{line}"
336           end
337         end
338       rescue Exception => e
339         Rails.logger.error "Background thread: Could not evaluate output: #{line.chomp}, exception: #{e}"
340         Rails.logger.error "Background thread: Backtrace: #{e.backtrace.join("\n")}"
341
342         # rethrow the exception
343         raise e
344       end
345     end
346
347     result
348   end
349
350   # just a short cut for accessing the singleton object
351   def self.bm
352     BackgroundManager.instance
353   end
354
355 end