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.
- data/README.rdoc +140 -175
- data/lib/robust_excel_ole/book.rb +3 -2
- data/lib/robust_excel_ole/excel.rb +162 -142
- data/lib/robust_excel_ole/reo_common.rb +2 -2
- data/lib/robust_excel_ole/version.rb +1 -1
- data/robust_excel_ole.gemspec +7 -2
- data/spec/book_spec.rb +2 -2
- data/spec/book_specs/book_open_spec.rb +5 -2
- data/spec/book_specs/book_save_spec.rb +5 -24
- data/spec/book_specs/book_sheet_spec.rb +1 -1
- data/spec/data/another_workbook.xls +0 -0
- data/spec/data/different_workbook.xls +0 -0
- data/spec/data/workbook.xls +0 -0
- data/spec/excel_spec.rb +232 -272
- data/spec/reo_common_spec.rb +1 -1
- metadata +12 -10
- data/README_detail.rdoc +0 -810
@@ -180,7 +180,8 @@ module RobustExcelOle
|
|
180
180
|
|
181
181
|
public
|
182
182
|
|
183
|
-
|
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
|
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 '
|
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
|
-
|
69
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
# :
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
:
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
-
#
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
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
|
-
|
198
|
-
|
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
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
260
|
+
raise first_error if (options[:if_unsaved] == :raise and first_error) or first_error.class == OptionInvalid
|
252
261
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
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
|
409
|
-
def self.
|
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 =
|
2
|
-
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
|
data/robust_excel_ole.gemspec
CHANGED
@@ -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
|
-
|
12
|
-
s.
|
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(
|
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(
|
341
|
+
}.to raise_error(ExcelError, "user canceled or runtime error")
|
342
342
|
@book.should be_alive
|
343
343
|
end
|
344
344
|
|