robust_excel_ole 0.6.2 → 1.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.
@@ -180,7 +180,8 @@ module RobustExcelOle
180
180
 
181
181
  public
182
182
 
183
- def self.open_in_current_excel(file, opts = { })
183
+ # work in progress#
184
+ def self.open_in_current_excel(file, opts = { }) # :nodoc: #
184
185
  options = DEFAULT_OPEN_OPTS.merge(opts)
185
186
  filename = General::absolute_path(file)
186
187
  ole_workbook = WIN32OLE.connect(filename)
@@ -325,7 +326,7 @@ module RobustExcelOle
325
326
  rescue WIN32OLERuntimeError => msg
326
327
  # trace "WIN32OLERuntimeError: #{msg.message}"
327
328
  if msg.message =~ /800A03EC/
328
- raise WorkbookError, "open: user canceled or open error"
329
+ raise ExcelError, "user canceled or runtime error"
329
330
  else
330
331
  raise UnexpectedError, "unknown WIN32OLERuntimeError"
331
332
  end
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- require 'timeout'
3
+ require 'weakref'
4
4
 
5
5
  def ka
6
6
  Excel.kill_all
@@ -62,11 +62,13 @@ module RobustExcelOle
62
62
 
63
63
  hwnd = ole_xl.HWnd
64
64
  stored = hwnd2excel(hwnd)
65
- if stored
65
+ if stored and stored.alive?
66
66
  result = stored
67
67
  else
68
- options[:visible] = options[:visible].nil? ? ole_xl.Visible : options[:visible]
69
- options[:displayalerts] = options[:displayalerts].nil? ? :if_visible : options[:displayalerts]
68
+ unless options.is_a? WIN32OLE
69
+ options[:visible] = options[:visible].nil? ? ole_xl.Visible : options[:visible]
70
+ options[:displayalerts] = options[:displayalerts].nil? ? :if_visible : options[:displayalerts]
71
+ end
70
72
  result = super(options)
71
73
  result.instance_variable_set(:@ole_excel, ole_xl)
72
74
  WIN32OLE.const_load(ole_xl, RobustExcelOle) unless RobustExcelOle.const_defined?(:CONSTANTS)
@@ -142,142 +144,172 @@ module RobustExcelOle
142
144
 
143
145
  public
144
146
 
145
- # closes the Excel
146
- # @param [Hash] options the options
147
- # @option options [Symbol] :if_unsaved :raise, :save, :forget, or :keep_open
148
- # @option options [Boolean] :hard
147
+ def self.contains_unsaved_workbooks?
148
+ excel = begin
149
+ Excel.current
150
+ rescue
151
+ return false
152
+ end
153
+ not excel.unsaved_workbooks.empty?
154
+ end
155
+
156
+ # returns unsaved workbooks
157
+ def unsaved_workbooks
158
+ unsaved_workbooks = []
159
+ begin
160
+ @ole_excel.Workbooks.each {|w| unsaved_workbooks << w unless (w.Saved || w.ReadOnly)}
161
+ rescue RuntimeError => msg
162
+ raise ExcelDamaged, "Excel instance not alive or damaged" if msg.message =~ /failed to get Dispatch Interface/
163
+ end
164
+ unsaved_workbooks
165
+ end
166
+
167
+ # closes workbooks
168
+ # @option options [Symbol] :if_unsaved :raise, :save, :forget, :alert, Proc
149
169
  # :if_unsaved if unsaved workbooks are open in an Excel instance
150
170
  # :raise (default) -> raises an exception
151
171
  # :save -> saves the workbooks before closing
152
172
  # :forget -> closes the Excel instance without saving the workbooks
153
- # :keep_open -> keeps the Excel instance open
154
- # :hard kill the Excel instance hard (default: false)
155
- def close(options = {})
156
- options = {
157
- :if_unsaved => :raise,
158
- :hard => false
159
- }.merge(options)
160
- close_excel(options) if managed_unsaved_workbooks(options)
161
- end
162
-
163
- # closes Excel instances opened via RobustExcelOle
164
- # @return [Fixnum] number of closed Excel instances
173
+ # :alert -> let Excel do it
174
+ def close_workbooks(options = {:if_unsaved => :raise})
175
+ return if not self.alive?
176
+ weak_wkbks = @ole_excel.Workbooks
177
+ if not unsaved_workbooks.empty? then
178
+ case options[:if_unsaved]
179
+ when Proc then
180
+ options[:if_unsaved].call(self, unsaved_workbooks)
181
+ when :raise then
182
+ raise UnsavedWorkbooks, "Excel contains unsaved workbooks"
183
+ when :alert then
184
+ #nothing
185
+ when :forget then
186
+ unsaved_workbooks.each {|m| m.Saved = true}
187
+ when :save then
188
+ unsaved_workbooks.each {|m| m.Save}
189
+ else
190
+ raise OptionInvalid, ":if_unsaved: invalid option: #{options[:if_unsaved].inspect}"
191
+ end
192
+ end
193
+ begin
194
+ @ole_excel.Workbooks.Close
195
+ rescue WIN32OLERuntimeError => msg
196
+ # trace "WIN32OLERuntimeError: #{msg.message}"
197
+ if msg.message =~ /800A03EC/
198
+ raise ExcelError, "user canceled or runtime error"
199
+ else
200
+ raise UnexpectedError, "unknown WIN32OLERuntimeError"
201
+ end
202
+ end
203
+ weak_wkbks = nil
204
+ weak_wkbks = @ole_excel.Workbooks
205
+ weak_wkbks = nil
206
+ end
207
+
208
+ # closes all Excel instances
209
+ # @return [Fixnum,Fixnum] number of closed Excel instances, number of errors
165
210
  # @param [Hash] options the options
166
211
  # @option options [Symbol] :if_unsaved :raise, :save, :forget, or :alert
167
- # @option options [Boolean] :hard
168
- # @option options [Boolean] :kill_if_timeout
169
212
  # options:
170
213
  # :if_unsaved if unsaved workbooks are open in an Excel instance
171
214
  # :raise (default) -> raises an exception
172
215
  # :save -> saves the workbooks before closing
173
216
  # :forget -> closes the excel instance without saving the workbooks
174
217
  # :alert -> give control to Excel
175
- # :hard closes Excel instances soft (default: false), or, additionally kills the Excel processes hard (true)
176
- # :kill_if_timeout: kills Excel instances hard if the closing process exceeds a certain time limit (default: false)
177
- def self.close_all(options={})
178
- options = {
179
- :if_unsaved => :raise,
180
- :hard => false,
181
- :kill_if_timeout => false
182
- }.merge(options)
183
- timeout = false
184
- number = @@hwnd2excel.size
185
- unsaved_workbooks = false
186
- begin
187
- status = Timeout::timeout(60) {
188
- @@hwnd2excel.each do |hwnd, wr_excel|
189
- excel = wr_excel.__getobj__
190
- begin
191
- excel.close(options)
192
- rescue UnsavedWorkbooks
193
- unsaved_workbooks = true
194
- end
195
- sleep 0.2
218
+ # @option options [Proc] block
219
+ def self.close_all(options = {:if_unsaved => :raise}, &blk)
220
+ options[:if_unsaved] = blk if blk
221
+ finished_number = error_number = overall_number = 0
222
+ first_error = nil
223
+ finishing_action = proc do |excel|
224
+ if excel
225
+ begin
226
+ overall_number += 1
227
+ finished_number += excel.close(:if_unsaved => options[:if_unsaved])
228
+ rescue
229
+ first_error = $!
230
+ #trace "error when finishing #{$!}"
231
+ error_number += 1
196
232
  end
197
- raise UnsavedWorkbooks, "Excel contains unsaved workbooks" if unsaved_workbooks
198
- free_all_ole_objects if excels_number > 0 #&& (not unsaved_workbooks)
199
- # close also interactively opened Excels, but for unsaved workbooks: hangs as soon sending a VBA method
200
- #while (n = excels_number) > 0 do
201
- # ole_xl = current_excel
202
- # begin
203
- # Excel.new(ole_xl).close(options) if ole_xl
204
- # rescue RuntimeError => msg
205
- # raise msg unless msg.message =~ /failed to get Dispatch Interface/
206
- # end
207
- #end
208
- }
209
- rescue Timeout::Error
210
- raise TimeOut, "close_all: timeout" unless options[:kill_if_timeout]
211
- timeout = true
212
- end
213
- kill_all if options[:hard] || (timeout && options[:kill_if_timeout])
214
- init
215
- number
216
- end
233
+ end
234
+ end
217
235
 
218
- def close_excel(options) # :nodoc:
219
- ole_xl = @ole_excel
220
- begin
221
- with_displayalerts(options[:if_unsaved] == :alert) { ole_xl.Workbooks.Close }
222
- rescue WIN32OLERuntimeError => msg
223
- trace "close: canceled by user" if msg.message =~ /80020009/ &&
224
- options[:if_unsaved] == :alert && (not unsaved_workbooks.empty?)
225
- end
226
- excel_hwnd = ole_xl.HWnd
227
- ole_xl.Quit
228
- weak_excel_ref = WeakRef.new(ole_xl)
229
- ole_xl = @ole_excel = nil
230
- GC.start
231
- sleep 0.2
232
- if weak_excel_ref.weakref_alive? then
233
- begin
234
- weak_excel_ref.ole_free
235
- #trace "successfully ole_freed #{weak_excel_ref}"
236
- rescue => msg
237
- trace "#{msg.message}"
238
- trace "could not do ole_free on #{weak_excel_ref}"
236
+ # known Excel-instances
237
+ @@hwnd2excel.each do |hwnd, wr_excel|
238
+ if wr_excel.weakref_alive?
239
+ excel = wr_excel.__getobj__
240
+ if excel.alive?
241
+ excel.displayalerts = false
242
+ finishing_action.call(excel)
243
+ end
244
+ else
245
+ @@hwnd2excel.delete(hwnd)
239
246
  end
240
247
  end
241
- @@hwnd2excel.delete(excel_hwnd)
242
- if options[:hard] then
243
- process_id = Win32API.new("user32", "GetWindowThreadProcessId", ["I","P"], "I")
244
- pid_puffer = " " * 32
245
- process_id.call(excel_hwnd, pid_puffer)
246
- pid = pid_puffer.unpack("L")[0]
247
- Process.kill("KILL", pid) rescue nil
248
+
249
+ # unknown Excel-instances
250
+ old_error_number = error_number
251
+ 9.times do |index|
252
+ sleep 0.1
253
+ excel = new(WIN32OLE.connect('Excel.Application')) rescue nil
254
+ finishing_action.call(excel) if excel
255
+ free_all_ole_objects unless error_number > 0 and options[:if_unsaved] == :raise
256
+ break if not excel
257
+ break if error_number > old_error_number # + 3
248
258
  end
249
- end
250
259
 
251
- private
260
+ raise first_error if (options[:if_unsaved] == :raise and first_error) or first_error.class == OptionInvalid
252
261
 
253
- def managed_unsaved_workbooks(options)
254
- unsaved_workbooks = []
255
- begin
256
- @ole_excel.Workbooks.each {|w| unsaved_workbooks << w unless (w.Saved || w.ReadOnly)}
257
- rescue RuntimeError => msg
258
- trace "RuntimeError: #{msg.message}"
259
- raise ExcelDamaged, "Excel instance not alive or damaged" if msg.message =~ /failed to get Dispatch Interface/
260
- end
261
- unless unsaved_workbooks.empty?
262
- case options[:if_unsaved]
263
- when :raise
264
- raise UnsavedWorkbooks, "Excel contains unsaved workbooks"
265
- when :save
266
- unsaved_workbooks.each do |workbook|
267
- workbook.Save
262
+ [finished_number, error_number]
263
+ end
264
+
265
+ # closes the Excel
266
+ # @param [Hash] options the options
267
+ # @option options [Symbol] :if_unsaved :raise, :save, :forget, :alert
268
+ # @option options [Boolean] :hard
269
+ # :if_unsaved if unsaved workbooks are open in an Excel instance
270
+ # :raise (default) -> raises an exception
271
+ # :save -> saves the workbooks before closing
272
+ # :forget -> closes the Excel instance without saving the workbooks
273
+ # :alert -> Excel takes over
274
+ def close(options = {:if_unsaved => :raise})
275
+ finishing_living_excel = self.alive?
276
+ if finishing_living_excel then
277
+ hwnd = (@ole_excel.HWnd rescue nil)
278
+ close_workbooks(:if_unsaved => options[:if_unsaved])
279
+ @ole_excel.Quit
280
+ if false and defined?(weak_wkbks) and weak_wkbks.weakref_alive? then
281
+ weak_wkbks.ole_free
282
+ end
283
+ weak_xl = WeakRef.new(@ole_excel)
284
+ else
285
+ weak_xl = nil
286
+ end
287
+ @ole_excel = nil
288
+ GC.start
289
+ sleep 0.1
290
+ if finishing_living_excel then
291
+ if hwnd then
292
+ process_id = Win32API.new("user32", "GetWindowThreadProcessId", ["I","P"], "I")
293
+ pid_puffer = " " * 32
294
+ process_id.call(hwnd, pid_puffer)
295
+ pid = pid_puffer.unpack("L")[0]
296
+ begin
297
+ Process.kill("KILL", pid)
298
+ rescue
299
+ #trace "kill_error: #{$!}"
300
+ end
301
+ end
302
+ @@hwnd2excel.delete(hwnd)
303
+ if weak_xl.weakref_alive? then
304
+ #if WIN32OLE.ole_reference_count(weak_xlapp) > 0
305
+ begin
306
+ weak_xl.ole_free
307
+ rescue
308
+ #trace "weakref_probl_olefree"
268
309
  end
269
- return true
270
- when :forget
271
- # nothing
272
- when :alert
273
- # nothing
274
- when :keep_open
275
- return false
276
- else
277
- raise OptionInvalid, ":if_unsaved: invalid option: #{options[:if_unsaved].inspect}"
278
310
  end
279
311
  end
280
- return true
312
+ weak_xl ? 1 : 0
281
313
  end
282
314
 
283
315
  # frees all OLE objects in the object space
@@ -306,8 +338,6 @@ module RobustExcelOle
306
338
  @@hwnd2excel = {}
307
339
  end
308
340
 
309
- public
310
-
311
341
  # kill all Excel instances
312
342
  # @return [Fixnum] number of killed Excel processes
313
343
  def self.kill_all
@@ -318,7 +348,7 @@ module RobustExcelOle
318
348
  begin
319
349
  Process.kill('KILL', p.processid) if p.name == "EXCEL.EXE"
320
350
  rescue
321
- trace "kill error: #{$!}"
351
+ #trace "kill error: #{$!}"
322
352
  end
323
353
  end
324
354
  init
@@ -335,12 +365,14 @@ module RobustExcelOle
335
365
  def self.excel_processes
336
366
  pid2excel = {}
337
367
  @@hwnd2excel.each do |hwnd,wr_excel|
338
- excel = wr_excel.__getobj__
339
- process_id = Win32API.new("user32", "GetWindowThreadProcessId", ["I","P"], "I")
340
- pid_puffer = " " * 32
341
- process_id.call(hwnd, pid_puffer)
342
- pid = pid_puffer.unpack("L")[0]
343
- pid2excel[pid] = excel
368
+ if wr_excel.weakref_alive?
369
+ excel = wr_excel.__getobj__
370
+ process_id = Win32API.new("user32", "GetWindowThreadProcessId", ["I","P"], "I")
371
+ pid_puffer = " " * 32
372
+ process_id.call(hwnd, pid_puffer)
373
+ pid = pid_puffer.unpack("L")[0]
374
+ pid2excel[pid] = excel
375
+ end
344
376
  end
345
377
  procs = WIN32OLE.connect("winmgmts:\\\\.")
346
378
  processes = procs.InstancesOf("win32_process")
@@ -405,28 +437,16 @@ module RobustExcelOle
405
437
  end
406
438
 
407
439
 
408
- # returns all unsaved workbooks in Excel instances
409
- def self.unsaved_workbooks_all # :nodoc: #
440
+ # returns unsaved workbooks in known (not opened by user) Excel instances
441
+ def self.unsaved_known_workbooks
410
442
  result = []
411
443
  @@hwnd2excel.each do |hwnd,wr_excel|
412
- excel = wr_excel.__getobj__
444
+ excel = wr_excel.__getobj__ if wr_excel.weakref_alive?
413
445
  result << excel.unsaved_workbooks
414
446
  end
415
447
  result
416
448
  end
417
449
 
418
- # returns unsaved workbooks
419
- def unsaved_workbooks
420
- result = []
421
- begin
422
- self.Workbooks.each {|w| result << w unless (w.Saved || w.ReadOnly)}
423
- rescue RuntimeError => msg
424
- trace "RuntimeError: #{msg.message}"
425
- raise ExcelDamaged, "Excel instance not alive or damaged" if msg.message =~ /failed to get Dispatch Interface/
426
- end
427
- result
428
- end
429
-
430
450
  def print_workbooks
431
451
  self.Workbooks.each {|w| trace "#{w.Name} #{w}"}
432
452
  end
@@ -1,5 +1,5 @@
1
- LOG_TO_STDOUT = true unless Object.const_defined?(:LOG_TO_STDOUT)
2
- REO_LOG_DIR = "" unless Object.const_defined?(:REO_LOG_DIR)
1
+ LOG_TO_STDOUT = false unless Object.const_defined?(:LOG_TO_STDOUT)
2
+ REO_LOG_DIR = "" unless Object.const_defined?(:REO_LOG_DIR)
3
3
  REO_LOG_FILE = "reo.log" unless Object.const_defined?(:REO_LOG_FILE)
4
4
 
5
5
  File.delete REO_LOG_FILE rescue nil
@@ -1,3 +1,3 @@
1
1
  module RobustExcelOle
2
- VERSION = "0.6.2"
2
+ VERSION = "1.0"
3
3
  end
@@ -8,9 +8,14 @@ Gem::Specification.new do |s|
8
8
  s.authors = ["traths"]
9
9
  s.email = ["Thomas.Raths@gmx.net"]
10
10
  s.homepage = "https://github.com/Thomas008/robust_excel_ole"
11
- s.summary = "RobustExcelOle processes Excel files and wraps the win32ole library."
12
- s.description = "RobustExcelOle processes Excel files, provides all win32ole operations, convenient methods for opening, saving and closing, and implements an Excel file management system."
11
+
12
+ s.summary = "RobustExcelOle automates processing Excel files in Windows by using the win32ole library."
13
+ s.description = "RobustExcelOle automates modifying, reading and writing Excel files.
14
+ It supports simultaneously running Excel instances and user interactions.
15
+ RobustExcelOle deals with various cases of Excel (and user) behaviour,
16
+ supplies workarounds for some Excel bugs, and supports referenced libraries"
13
17
 
18
+ s. licenses = ['MIT']
14
19
  s.rubyforge_project = "robust_excel_ole"
15
20
 
16
21
  s.files = `git ls-files`.split("\n")
data/spec/book_spec.rb CHANGED
@@ -316,7 +316,7 @@ describe Book do
316
316
  @key_sender.puts "{right}{enter}"
317
317
  expect{
318
318
  Book.open(@simple_file, :if_unsaved => :alert)
319
- }.to raise_error(WorkbookError, "open: user canceled or open error")
319
+ }.to raise_error(ExcelError, "user canceled or runtime error")
320
320
  @book.should be_alive
321
321
  end
322
322
 
@@ -338,7 +338,7 @@ describe Book do
338
338
  @key_sender.puts "{right}{enter}"
339
339
  expect{
340
340
  Book.open(@simple_file, :if_unsaved => :excel)
341
- }.to raise_error(WorkbookError, "open: user canceled or open error")
341
+ }.to raise_error(ExcelError, "user canceled or runtime error")
342
342
  @book.should be_alive
343
343
  end
344
344