dde 0.2.2

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.
@@ -0,0 +1,33 @@
1
+ module DDE
2
+
3
+ # Class encapsulates DDE string. In addition to normal string behavior,
4
+ # it also has *handle* that can be passed to dde functions
5
+ class DdeString < String
6
+ include Win::DDE
7
+
8
+ attr_accessor :handle, # string handle passable to DDEML functions
9
+ :instance_id, # instance id of DDE app that created this DdeString
10
+ :code_page, # Windows code page for this string (CP_WINANSI or CP_WINUNICODE)
11
+ :name # ORIGINAL string used to create this DdeString
12
+
13
+ # Given the DDE application instance_id, you cane create DdeStrings
14
+ # either from regular string or from known DdeString handle
15
+ def initialize(instance_id, string_or_handle, code_page=CP_WINANSI)
16
+ @instance_id = instance_id
17
+ @code_page = code_page
18
+
19
+ begin
20
+ if string_or_handle.is_a? String
21
+ @name = string_or_handle
22
+ error unless @handle = dde_create_string_handle(@instance_id, @name, @code_page)
23
+ else
24
+ @handle = string_or_handle
25
+ error unless @name = dde_query_string(@instance_id, @handle, @code_page)
26
+ end
27
+ rescue => e
28
+ end
29
+ raise DDE::Errors::StringError, "Failed to initialize DDE string: #{e}" unless @handle && @name && !e
30
+ super @name
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ module DDE
2
+
3
+ # Class encapsulates DDE Monitor that prints all DDE transactions to console
4
+ class Monitor < App
5
+
6
+ # Creates new DDE monitor instance
7
+ def initialize(init_flags=nil, &callback)
8
+ init_flags ||=
9
+ APPCLASS_MONITOR | # this is monitor
10
+ MF_CALLBACKS | # monitor callback functions
11
+ MF_CONV | # monitor conversation data
12
+ MF_ERRORS | # monitor DDEML errors
13
+ MF_HSZ_INFO | # monitor data handle activity
14
+ MF_LINKS | # monitor advise loops
15
+ MF_POSTMSGS | # monitor posted DDE messages
16
+ MF_SENDMSGS # monitor sent DDE messages
17
+
18
+ callback ||= lambda do |*args|
19
+ p args.unshift(Win::DDE::TYPES[args.shift]).push(Win::DDE::FLAGS[args.pop])
20
+ 1
21
+ end
22
+
23
+ super init_flags, &callback
24
+ end
25
+ end
26
+ end
data/lib/dde/server.rb ADDED
@@ -0,0 +1,41 @@
1
+ module DDE
2
+
3
+ # Class encapsulates DDE Server with basic functionality (starting/stopping named service)
4
+ class Server < App
5
+
6
+ attr_reader :service # service(s) that this Server supports
7
+
8
+ def start_service( name, init_flags=nil, &dde_callback )
9
+ try "Starting service #{name}", DDE::Errors::ServiceError do
10
+ # Trying to start DDE if it was inactive
11
+ error unless dde_active? || start_dde( init_flags, &dde_callback )
12
+
13
+ # Create DDE string for name (this creates handle that can be passed to DDEML functions)
14
+ @service = DDE::DdeString.new(@id, name)
15
+
16
+ # Register new DDE service, returns true/false success code
17
+ error unless dde_name_service(@id, @service.handle, DNS_REGISTER)
18
+ end
19
+ end
20
+
21
+ def stop_service
22
+ try "Stopping active service", DDE::Errors::ServiceError do
23
+ error "Either DDE or service not initialized" unless dde_active? && service_active?
24
+
25
+ # Unregister DDE service, returns true/false success code
26
+ error unless dde_name_service(@id, @service.handle, DNS_UNREGISTER);
27
+
28
+ # Free string handle for service name
29
+ error unless dde_free_string_handle(@id, @service.handle)
30
+
31
+ # Clear handle if service successfuly stopped
32
+ @service = nil
33
+ end
34
+ end
35
+
36
+ def service_active?
37
+ !!@service
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ require 'dde/xl_table'
2
+
3
+ module DDE
4
+
5
+ # Class encapsulates DDE Server mimicking Excel. It is used to create DDE server with specific service name
6
+ # (default name 'excel') and store data received by the server via DDE
7
+ class XlServer < Server
8
+
9
+ attr_reader :format, # data format(s) (registered clipboard formats) that server supports
10
+ :table # data table for data storage
11
+
12
+ # Creates new Xl Server instance
13
+ def initialize(init_flags = nil, &dde_callback )
14
+
15
+ @table = DDE::XlTable.new
16
+
17
+ # Trying to register or retrieve existing format XlTable
18
+ try 'Registering format XlTable', DDE::Errors::FormatError do
19
+ @format = register_clipboard_format("XlTable")
20
+ end
21
+
22
+ super init_flags, &dde_callback
23
+ end
24
+
25
+ # Make 'excel' the default name for named service
26
+ alias_method :__start_service, :start_service
27
+ def start_service( name='excel', init_flags=nil, &dde_callback )
28
+ __start_service( name, init_flags, &dde_callback )
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,172 @@
1
+ module DDE
2
+ extend FFI::Library # todo: < Array ?
3
+ class Conv < FFI::Union
4
+ layout( :w, :ushort, # word should be 2 bytes, not 8
5
+ :d, :double, # it is 8 bytes
6
+ :b, [:char, 8]) # it is 8 bytes
7
+ end
8
+
9
+
10
+ # XLTable class represents a single chunk of DDE data formatted as an Excel table
11
+ class XlTable
12
+ include Win::DDE
13
+
14
+ # Received data types
15
+ TDT_FLOAT = 1
16
+ TDT_STRING = 2
17
+ TDT_BOOL = 3
18
+ TDT_ERROR = 4
19
+ TDT_BLANK = 5
20
+ TDT_INT = 6
21
+ TDT_SKIP = 7
22
+ TDT_TABLE = 16
23
+
24
+ TDT_TYPES = {
25
+ TDT_FLOAT => 'TDT_FLOAT',
26
+ TDT_STRING =>'TDT_STRING',
27
+ TDT_BOOL => 'TDT_BOOL',
28
+ TDT_ERROR => 'TDT_ERROR',
29
+ TDT_BLANK => 'TDT_BLANK',
30
+ TDT_INT => 'TDT_INT',
31
+ TDT_SKIP => 'TDT_SKIP',
32
+ TDT_TABLE => 'TDT_TABLE'
33
+ }
34
+
35
+ attr_accessor :topic_item # topic_item
36
+
37
+ def initialize
38
+ @table_data = [] # Array contains Arrays of Strings
39
+ @col = 0
40
+ @row = 0
41
+ # omitting separators for now
42
+ end
43
+
44
+ # tests if table data is empty or contains data in inconsistent state
45
+ def empty?
46
+ @table_data.empty? ||
47
+ @row == 0 || @col == 0 ||
48
+ @row != @table_data.size ||
49
+ @col != @table_data.first.size # assumes first element is also an Array
50
+ end
51
+
52
+ def data?;
53
+ !empty?
54
+ end
55
+
56
+ def draw
57
+ return false if empty?
58
+
59
+ # omitting separator gymnastics for now
60
+ cout "-----\n"
61
+ @table_data.each{|row| row.each {|col| cout col, " "}; cout "\n"}
62
+ end
63
+
64
+ def get_data(dde_handle)
65
+ # conv = DDE::Conv.new # Union for data conversion
66
+
67
+ # Copy DDE data from dde_handle (FFI::MemoryPointer is returned)
68
+ return nil unless data = dde_get_data(dde_handle) # raise 'DDE data not extracted'
69
+ offset = 0
70
+
71
+ # Make sure that the first block is tdtTable
72
+ return nil unless data.get_int16(offset) == TDT_TABLE # raise 'DDE data not TDT_TABLE'
73
+ offset += 2
74
+
75
+ # Make sure cb == 4
76
+ return nil unless data.get_int16(offset) == 4 # raise 'TDT_TABLE data length wrong'
77
+ offset += 2
78
+
79
+ @row = data.get_int16(offset)
80
+ @col = data.get_int16(offset+2)
81
+ offset += 4
82
+ #p "row, col", @row, @col
83
+
84
+ # Make sure nonzero row and col
85
+ return nil if @row == 0 || @col == 0 # raise 'col or row zero in TDT_TABLE'
86
+
87
+ @table_data = Array.new(@row){||Array.new}
88
+
89
+ r = 0
90
+ c = 0
91
+ while offset < data.size
92
+ type = data.get_int16(offset) # Next data field(s) type
93
+ cb = data.get_int16(offset+2) # Next data field(s) length in bytes
94
+ offset += 4
95
+
96
+ #p "type #{TDT_TYPES[type]}, cb #{cb}, row #{r}, col #{c}"
97
+ case type
98
+ when TDT_FLOAT # Float, 8 bytes per field
99
+ (cb/8).times do
100
+ @table_data[r][c] = data.get_float64(offset)
101
+ offset += 8
102
+ c += 1
103
+ if c == @col # end of row
104
+ c = 0
105
+ r += 1
106
+ end
107
+ end
108
+ when TDT_STRING
109
+ end_field = offset + cb
110
+ while offset < end_field do
111
+ length = data.get_int8(offset)
112
+ offset += 1
113
+ @table_data[r][c] = data.get_bytes(offset, length)
114
+ offset += length
115
+ c += 1
116
+ if c == @col # end of row
117
+ c = 0
118
+ r += 1
119
+ end
120
+ end
121
+ when TDT_BOOL # Bool, 2 bytes per field
122
+ (cb/2).times do
123
+ @table_data[r][c] = data.get_int16(offset) == 0
124
+ offset += 2
125
+ c += 1
126
+ if c == @col # end of row
127
+ c = 0
128
+ r += 1
129
+ end
130
+ end
131
+ when TDT_ERROR # Error enum, 2 bytes per field
132
+ (cb/2).times do
133
+ @table_data[r][c] = "Error:#{data.get_int16(offset)}"
134
+ offset += 2
135
+ c += 1
136
+ if c == @col # end of row
137
+ c = 0
138
+ r += 1
139
+ end
140
+ end
141
+ when TDT_BLANK # Number of blank cells, 2 bytes per field
142
+ (cb/2).times do
143
+ blanks = data.get_int16(offset)
144
+ offset += 2
145
+ blanks.times do
146
+ @table_data[r][c] = ""
147
+ c += 1
148
+ if c == @col # end of row
149
+ c = 0
150
+ r += 1
151
+ end
152
+ end
153
+ end
154
+ when TDT_INT # Int, 2 bytes per field
155
+ (cb/2).times do
156
+ @table_data[r][c] = data.get_int16(offset) == 0
157
+ offset += 2
158
+ c += 1
159
+ if c == @col # end of row
160
+ c = 0
161
+ r += 1
162
+ end
163
+ end
164
+ else
165
+ return nil
166
+ end
167
+ end
168
+ #TODO: free FFI::Pointer ? delete []data; // Free memory
169
+ true # Data acquisition successful
170
+ end
171
+ end
172
+ end
data/lib/dde.rb ADDED
@@ -0,0 +1,8 @@
1
+ # console output redirection (may need to wrap it in synchronization code, etc)
2
+ require 'win/dde'
3
+ require 'dde/dde_string'
4
+ require 'dde/app'
5
+ require 'dde/server'
6
+ require 'dde/client'
7
+ require 'dde/monitor'
8
+ require 'dde/xl_server'
@@ -0,0 +1,85 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module DDETest
4
+ describe DDE::App do
5
+ before(:each ){ @app = DDE::App.new }
6
+
7
+ it 'starts with nil id and flags if no arguments given' do
8
+ @app.id.should == nil
9
+ @app.init_flags.should == nil
10
+ @app.dde_active?.should == false
11
+ end
12
+
13
+ it 'starts DDE (initializes as STANDARD DDE app) with given callback block' do
14
+ app = DDE::App.new {|*args|}
15
+ app.id.should be_an Integer
16
+ app.id.should_not == 0
17
+ app.init_flags.should == APPCLASS_STANDARD
18
+ app.dde_active?.should == true
19
+ end
20
+
21
+ describe '#start_dde' do
22
+ it 'starts DDE with callback and default init_flags' do
23
+ res = @app.start_dde {|*args|}
24
+ res.should be_true
25
+ @app.id.should be_an Integer
26
+ @app.id.should_not == 0
27
+ @app.init_flags.should == APPCLASS_STANDARD
28
+ @app.dde_active?.should == true
29
+ end
30
+
31
+ it 'returns self if success (allows method chain)' do
32
+ @app.start_dde {|*args|}.should == @app
33
+ end
34
+
35
+ it 'starts DDE with callback and given init_flags' do
36
+ res = @app.start_dde( APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS ){|*args|}
37
+ res.should be_true
38
+ @app.id.should be_an Integer
39
+ @app.id.should_not == 0
40
+ @app.init_flags.should == APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS
41
+ @app.dde_active?.should == true
42
+ end
43
+
44
+ it 'raises InitError if no callback was given' do
45
+ lambda{ @app.start_dde}.should raise_error DDE::Errors::InitError
46
+ end
47
+
48
+ it 'reinitializes with new flags and callback if it was already initialized' do
49
+ @app.start_dde {|*args| 1}
50
+ old_id = @app.id
51
+ res = @app.start_dde( APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS ){|*args| 2}
52
+ res.should be_true
53
+ @app.id.should == old_id
54
+ @app.init_flags.should == APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS
55
+ @app.dde_active?.should == true
56
+ end
57
+ end
58
+
59
+ describe '#stop_dde' do
60
+ it 'stops DDE that was active' do
61
+ @app.start_dde {|*args| 1}
62
+
63
+ @app.stop_dde
64
+ @app.id.should == nil
65
+ @app.dde_active?.should == false
66
+ end
67
+
68
+ it 'preserves init_flags after DDE is stopped (for reinitialization)' do
69
+ @app.start_dde(APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS) {|*args| 1}
70
+
71
+ @app.stop_dde
72
+ @app.init_flags.should == APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS
73
+ end
74
+
75
+ it 'returns self if success (allows method chain)' do
76
+ @app.start_dde{|*args|}.stop_dde.should == @app
77
+ end
78
+
79
+ it 'raises InitError if dde was not active first' do
80
+ lambda{ @app.stop_dde}.should raise_error DDE::Errors::InitError
81
+ end
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,176 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module DDETest
4
+
5
+ describe DDE::Client do
6
+ # before(:all){@monitor = DDE::Monitor.new}
7
+ # after(:all){@monitor.stop_dde}
8
+
9
+ before(:each ){ @client = DDE::Client.new }
10
+ # after(:each ){ @client.stop_dde}
11
+
12
+ it 'new without parameters creates Client but does not activate DDEML' do
13
+ @client.id.should == nil
14
+ @client.conversation.should == nil
15
+ @client.service.should == nil
16
+ @client.topic.should == nil
17
+ @client.dde_active?.should == false
18
+ @client.conversation_active?.should == false
19
+ end
20
+
21
+ it 'new with attached callback block creates Client and activates DDEML' do
22
+ client = DDE::Client.new {|*args|}
23
+ client.id.should be_an Integer
24
+ client.id.should_not == 0
25
+ client.dde_active?.should == true
26
+ client.conversation.should == nil
27
+ client.conversation_active?.should == false
28
+ client.service.should == nil
29
+ client.topic.should == nil
30
+ end
31
+
32
+ describe '#start_conversation' do
33
+
34
+ context 'with inactive (uninitialized) DDE:' do
35
+ it 'fails to starts new conversation' do
36
+ lambda{@client.start_conversation('service', 'topic')}.
37
+ should raise_error /DDE is not initialized/
38
+ @client.conversation_active?.should == false
39
+ lambda{@client.start_conversation(nil, nil)}.
40
+ should raise_error /DDE is not initialized/
41
+ @client.conversation_active?.should == false
42
+ lambda{@client.start_conversation}.
43
+ should raise_error /DDE is not initialized/
44
+ @client.conversation_active?.should == false
45
+ end
46
+ end
47
+
48
+ context 'with active (initialized) DDE AND existing DDE server supporting "service" topic' do
49
+ before(:each )do
50
+ @client_calls = []
51
+ @server_calls = []
52
+ @client = DDE::Client.new {|*args| @client_calls << args; 1}
53
+ @server = DDE::Server.new {|*args| @server_calls << args; 1}.start_service('service')
54
+ end
55
+
56
+ it 'starts new conversation if DDE is already activated' do
57
+ res = @client.start_conversation 'service', 'topic'
58
+ res.should be_true
59
+ @client.conversation_active?.should == true
60
+ end
61
+
62
+ it 'returns self if success (allows method chain)' do
63
+ @client.start_conversation('service', 'topic').should == @client
64
+ end
65
+
66
+ it 'sets @conversation, @service and @topic attributes' do
67
+ @client.start_conversation 'service', 'topic'
68
+
69
+ @client.conversation.should be_an Integer
70
+ @client.conversation.should_not == 0
71
+ @client.service.should be_a DDE::DdeString
72
+ @client.service.should == 'service'
73
+ @client.service.name.should == 'service'
74
+ @client.conversation.should be_an Integer
75
+ @client.conversation.should_not == 0
76
+ end
77
+
78
+ it 'initiates XTYP_CONNECT transaction to service`s callback' do
79
+ @client.start_conversation 'service', 'topic'
80
+
81
+ @server_calls.first[0].should == XTYP_CONNECT
82
+ @server_calls.first[3].should == @client.topic.handle
83
+ @server_calls.first[4].should == @client.service.handle
84
+ end
85
+
86
+ it 'if server confirms connect, XTYP_CONNECT_CONFIRM transaction to service`s callback follows' do
87
+ @client.start_conversation 'service', 'topic'
88
+
89
+ @server_calls[1][0].should == XTYP_CONNECT_CONFIRM
90
+ @server_calls[1][3].should == @client.topic.handle
91
+ @server_calls[1][4].should == @client.service.handle
92
+ end
93
+
94
+ it 'client`s callback receives no transactions' do
95
+ @client.start_conversation 'service', 'topic'
96
+
97
+ p @server_calls, @client.service.handle, @client.topic.handle, @client.conversation
98
+ @client_calls.should == []
99
+ end
100
+
101
+ it 'fails if another conversation is already in progress' do
102
+ @client.start_conversation 'service', 'topic'
103
+
104
+ lambda{@client.start_conversation 'service1', 'topic1'}.
105
+ should raise_error /Another conversation already established/
106
+ end
107
+
108
+ it 'fails to start conversation on unsupported service' do
109
+ lambda{@client.start_conversation('not_a_service', 'topic')}.
110
+ should raise_error /A client`s attempt to establish a conversation has failed/
111
+ @client.conversation_active?.should == false
112
+ end
113
+
114
+ end
115
+ end
116
+
117
+ describe '#stop_conversation' do
118
+
119
+ context 'with inactive (uninitialized) DDE:' do
120
+ it 'fails to stop conversation' do
121
+ lambda{@client.stop_conversation}.
122
+ should raise_error /DDE not started/
123
+ @client.conversation_active?.should == false
124
+ end
125
+
126
+ end
127
+
128
+ context 'with active (initialized) DDE AND existing DDE server supporting "service" topic' do
129
+ before(:each )do
130
+ @client_calls = []
131
+ @server_calls = []
132
+ @client = DDE::Client.new {|*args| @client_calls << args; 1}
133
+ @server = DDE::Server.new {|*args| @server_calls << args; 1}
134
+ @server.start_service('service')
135
+ end
136
+
137
+ it 'fails to stop conversation' do
138
+ lambda{@client.stop_conversation}.
139
+ should raise_error /Conversation not started/
140
+ @client.conversation_active?.should == false
141
+ end
142
+
143
+ context 'conversation already started' do
144
+ before(:each ){@client.start_conversation 'service', 'topic'}
145
+
146
+ it 'stops conversation' do
147
+ res = @client.stop_conversation
148
+ res.should be_true
149
+ @client.conversation_active?.should == false
150
+ end
151
+
152
+ it 'unsets @conversation, @service and @topic attributes' do
153
+ @client.stop_conversation
154
+ @client.conversation.should == nil
155
+ @client.service.should == nil
156
+ @client.topic.should == nil
157
+ end
158
+
159
+ it 'does not stop DDE' do
160
+ @client.stop_conversation
161
+ @client.dde_active?.should == true
162
+ end
163
+
164
+ it 'initiates XTYP_DISCONNECT transaction to service`s callback' do
165
+ pending
166
+ @client.stop_conversation
167
+ p @server_calls, @client_calls # ?????????? No XTYP_DISCONNECT ? Why ?
168
+ @server_calls.last[0].should == XTYP_DISCONNECT
169
+ end
170
+
171
+ end
172
+
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,40 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ module DDETest
4
+ describe DDE::DdeString do
5
+ before(:each ){ @app = DDE::App.new {|*args|}}
6
+
7
+ context ' with valid instance id of active DDE application' do
8
+ it 'can be created from normal string' do
9
+ dde_string = DDE::DdeString.new(@app.id, "My_String")
10
+ dde_string == "My_String"
11
+ dde_string.handle.should be_an Integer
12
+ dde_string.handle.should_not == 0
13
+ end
14
+
15
+ it 'can be created from valid DDE string handle' do
16
+ string_handle = dde_create_string_handle(@app.id, 'My String')
17
+ dde_string = DDE::DdeString.new(@app.id, string_handle)
18
+ dde_string == "My_String"
19
+ dde_string.handle.should be_an Integer
20
+ dde_string.handle.should_not == 0
21
+ end
22
+ end
23
+
24
+ context ' without instance id of active DDE application' do
25
+ it 'cannot be created from String' do
26
+ lambda{DDE::DdeString.new(nil, "My_String")}.should raise_error DDE::Errors::StringError
27
+ lambda{DDE::DdeString.new(12345, "My_String")}.should raise_error DDE::Errors::StringError
28
+ lambda{DDE::DdeString.new(0, "My_String")}.should raise_error DDE::Errors::StringError
29
+ end
30
+
31
+ it 'cannot be created from valid string handle' do
32
+ string_handle = dde_create_string_handle(@app.id, 'My String')
33
+ lambda{DDE::DdeString.new(nil, string_handle)}.should raise_error DDE::Errors::StringError
34
+ lambda{DDE::DdeString.new(12345, string_handle)}.should raise_error DDE::Errors::StringError
35
+ lambda{DDE::DdeString.new(0, string_handle)}.should raise_error DDE::Errors::StringError
36
+ end
37
+ end
38
+
39
+ end
40
+ end