robust_excel_ole 0.6.2 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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