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.
- 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
|
|