fixed Patch installation (bnc#579034), fixed possible race condition
[opensuse:yast-rest-service.git] / plugins / software / app / models / patch.rb
1 require 'resolvable'
2
3 # Model for patches available via package kit
4 class Patch < Resolvable
5
6   private
7
8   # just a short cut for accessing the singleton object
9   def self.bm
10     BackgroundManager.instance
11   end
12
13   # create unique id for the background manager
14   def self.id(what)
15     "patches_#{what}"
16   end
17
18   public
19
20   def to_xml( options = {} )
21     super :patch_update, options
22   end
23
24   # find patches using PackageKit
25   def self.do_find(what, bg_status = nil)
26     patch_updates = Array.new
27     PackageKit.transact("GetUpdates", "NONE", "Package", bg_status) { |line1,line2,line3|
28       columns = line2.split ";"
29       if what == :available || columns[1] == what
30         update = Patch.new(:resolvable_id => columns[1],
31                            :kind => line1,
32                            :name => columns[0],
33                            :arch => columns[2],
34                            :repo => columns[3],
35                            :summary => line3 )
36         return update if columns[1] == what #only the first entry will be returned in a hash
37         patch_updates << update
38       end
39     }
40     return patch_updates
41   end
42
43   def self.subprocess_find(what)
44     # open subprocess
45     subproc = open_subprocess what
46
47     result = nil
48
49     while !eof_subprocess?(subproc) do
50       begin
51         line = read_subprocess subproc
52
53         unless line.blank?
54           received = Hash.from_xml(line)
55
56           # is it a progress or the final list?
57           if received.has_key? 'patches'
58             Rails.logger.debug "Found #{received['patches'].size} patches"
59             # create Patch objects
60             result = received['patches'].map{|patch| Patch.new(patch.symbolize_keys) }
61           elsif received.has_key? 'background_status'
62             s = received['background_status']
63
64             bm.update_progress id(what) do |bs|
65               bs.status = s['status']
66               bs.progress = s['progress']
67               bs.subprogress = s['subprogress']
68             end
69           elsif received.has_key? 'error'
70             return PackageKitError.new(received['error']['description']) if received['error']['type'] == 'PACKAGEKIT_ERROR'
71             Rails.logger.warn "*** Patch thread: Received unknown error: #{received['error'].inspect}"
72             return BackendException.new(received['error']['description'])
73           else
74             Rails.logger.warn "*** Patch thread: Received unknown input: #{line}"
75           end
76         end
77       rescue Exception => e
78         Rails.logger.error "Background thread: Could not evaluate output: #{line.chomp}, exception: #{e}"
79         Rails.logger.error "Background thread: Backtrace: #{e.backtrace.join("\n")}"
80
81         # rethrow the exception
82         raise e
83       end
84     end
85
86     result
87   end
88
89
90   # find patches
91   # Patch.find(:available)
92   # Patch.find(:available, :background => true) - read patches in background
93   #   the result may the current state (progress) or the actual patch list
94   #   call this function in a loop until a patch list (or an error) is received
95   # Patch.find(212)
96   def self.find(what, opts = {})
97     background = opts[:background]
98
99     # background reading doesn't work correctly if class reloading is active
100     # (static class members are lost between requests)
101     if background && !bm.background_enabled?
102       Rails.logger.info "Class reloading is active, cannot use background thread (set config.cache_classes = true)"
103       background = false
104     end
105
106     if background
107       proc_id = id(what)
108       if bm.process_finished? proc_id
109         Rails.logger.debug "Request #{proc_id} is done"
110         ret = bm.get_value proc_id
111
112         # check for exception
113         if ret.is_a? StandardError
114           raise ret
115         end
116
117         return ret
118       end
119
120       running = bm.get_progress proc_id
121       if running
122         Rails.logger.debug "Request #{proc_id} is already running: #{running.inspect}"
123         return [running]
124       end
125
126
127       bm.add_process proc_id
128
129       Rails.logger.info "Starting background thread for reading patches..."
130       # run the patch query in a separate thread
131       Thread.new do
132         res = subprocess_find what
133
134         # check for exception
135         unless res.is_a? StandardError
136           Rails.logger.info "*** Patches thread: Found #{res.size} applicable patches"
137         else
138           Rails.logger.debug "*** Exception raised: #{res.inspect}"
139         end
140         bm.finish_process(proc_id, res)
141       end
142
143       return [ bm.get_progress(proc_id) ]
144     else
145       return do_find(what)
146     end
147   end
148
149   # Patch.install(patch)
150   # Patch.install(id)
151   def self.install(patch)
152     if patch.is_a?(Patch)
153       update_id = "#{patch.name};#{patch.resolvable_id};#{patch.arch};#{patch.repo}"
154       Rails.logger.debug "Install Update: #{update_id}"
155       PackageKit.install update_id
156     else
157       # if is not an object, assume it is an id
158       patch_id = patch
159       patch = Patch.find(patch_id)
160       raise "Can't install update #{patch_id} because it does not exist" if patch.nil? or not patch.is_a?(Patch)
161       self.install(patch)
162     end
163   end
164
165   private
166
167   def self.subprocess_script
168     # find the helper script
169     script = File.join(RAILS_ROOT, 'vendor/plugins/software/scripts/list_patches.rb')
170
171     unless File.exists? script
172       script = File.join(RAILS_ROOT, '../plugins/software/scripts/list_patches.rb')
173
174       unless File.exists? script
175         raise 'File software/scripts/list_patches.rb was not found!'
176       end
177     end
178
179     Rails.logger.debug "Using #{script} script file"
180     script
181   end
182
183   def self.subprocess_command(what)
184     raise "Invalid parameter" if what.to_s.include?("'") or what.to_s.include?('\\')
185     ret = "cd #{RAILS_ROOT} && #{File.join(RAILS_ROOT, 'script/runner')} -e #{ENV['RAILS_ENV'] || 'development'} #{subprocess_script}"
186     ret += " #{what}" if what != :available
187     ret
188   end
189
190   # IO functions moved to separate methods for easy mocking/testing
191
192   def self.open_subprocess(what)
193     IO.popen(subprocess_command what)
194   end
195
196   def self.read_subprocess(subproc)
197     subproc.readline
198   end
199
200   def self.eof_subprocess?(subproc)
201     subproc.eof?
202   end
203
204 end