dde 0.2.2 → 0.2.8

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/lib/dde/xl_server.rb CHANGED
@@ -22,9 +22,90 @@ module DDE
22
22
  super init_flags, &dde_callback
23
23
  end
24
24
 
25
+ # HDDEDATA CALLBACK DdeCallback(UINT uType, UINT uFmt, HCONV hConv, HSZ hsz1, HSZ hsz2,
26
+ # HDDEDATA hData, DWORD dwData1, DWORD dwData2)
27
+ def default_callback
28
+ lambda do |type, format, conv, hsz1, hsz2, data, data1, data2|
29
+ case type
30
+ when XTYP_CONNECT # Request to connect from client, creating data exchange channel
31
+ # format:: Not used.
32
+ # conv:: Not used.
33
+ # hsz1:: Handle to the topic name.
34
+ # hsz2:: Handle to the service name.
35
+ # data:: Handle to DDE data. Meaning of this parameter depends on the type of the current transaction.
36
+ # data1:: Pointer to a CONVCONTEXT structure that contains context information for the conversation.
37
+ # If the client is not a Dynamic Data Exchange Management Library (DDEML) application,
38
+ # this parameter is 0.
39
+ # data2:: Specifies whether the client is the same application instance as the server. If the
40
+ # parameter is 1, the client is the same instance. If the parameter is 0, the client
41
+ # is a different instance.
42
+ # *Returns*:: A server callback function should return TRUE(1) to allow the client to establish a
43
+ # conversation on the specified service name and topic name pair, or the function
44
+ # should return FALSE to deny the conversation. If the callback function returns TRUE(1)
45
+ # and a conversation is successfully established, the system passes the conversation
46
+ # todo: handle to the server by issuing an XTYP_CONNECT_CONFIRM transaction to the server's
47
+ # callback function (unless the server specified the CBF_SKIP_CONNECT_CONFIRMS flag
48
+ # in the DdeInitialize function).
49
+
50
+ if hsz2 == @service.handle
51
+ cout "Service #{@service}: connect requested by client\n"
52
+ 1 # instead of true # Yes, this server supports requested (name) handle
53
+ else
54
+ cout "Service #{@service} unable to process connection request for #{hsz2}\n"
55
+ DDE_FNOTPROCESSED # 0 instead of false # No, server does not support requested (name) handle
56
+ end
57
+
58
+ when XTYP_POKE # Client initiated XTYP_POKE transaction to push unsolicited data to the server
59
+ # format:: Specifies the format of the data sent from the server.
60
+ # conv:: Handle to the conversation.
61
+ # hsz1:: Handle to the topic name. (Excel: [topic]item ?!)
62
+ # hsz2:: Handle to the item name.
63
+ # data_handle:: Handle to the data that the client is sending to the server.
64
+ # *Returns*:: A server callback function should return the DDE_FACK flag if it processes this
65
+ # transaction, the DDE_FBUSY flag if it is too busy to process this transaction,
66
+ # or the DDE_FNOTPROCESSED flag if it rejects this transaction.
67
+
68
+ if @table.get_data(data) # Extract incoming DDE data from client into server's table
69
+ # Converting hsz1 into "[topic]item" string and
70
+ @table.topic = dde_query_string(@id, hsz1)
71
+ # @table.draw # Simply printing it for now, no queues
72
+ @table.timer
73
+ # // Placing table into print queue
74
+ # WaitForSingleObject(hMutex1,INFINITE);
75
+ # q.push(server.xltable);
76
+ # ReleaseMutex(hMutex1);
77
+ # // Allowing the table output thread to start...
78
+ # ReleaseSemaphore(hSemaphore,1,NULL);
79
+ #
80
+ DDE_FACK # Transaction successful
81
+ else
82
+ cout "Service #{@service} unable to process data request (XTYP_POKE) for #{hsz2}"
83
+ DDE_FNOTPROCESSED # 0 Transaction NOT successful - return (HDDEDATA)TRUE; ?!(why TRUE, not FALSE)
84
+ end
85
+
86
+ when XTYP_DISCONNECT # DDE client disconnects
87
+ # server.xltable.Delete();
88
+ # break;
89
+ DDE_FNOTPROCESSED # 0 - return((HDDEDATA)NULL);// is it the same as 0 ?!
90
+
91
+ when XTYP_ERROR # DDE Error
92
+ # WaitForSingleObject(hMutex, INFINITE);
93
+ # std::cerr<<"DDE error.\n";
94
+ # ReleaseMutex(hMutex);
95
+ # break;
96
+ DDE_FNOTPROCESSED # 0 - return((HDDEDATA)NULL);// is it the same as 0 ?!
97
+
98
+ else
99
+ DDE_FNOTPROCESSED # 0 - return((HDDEDATA)NULL);// is it the same as 0 ?!
100
+ end
101
+ end
102
+ end
103
+
25
104
  # Make 'excel' the default name for named service
26
105
  alias_method :__start_service, :start_service
27
- def start_service( name='excel', init_flags=nil, &dde_callback )
106
+
107
+ def start_service( name='excel', init_flags=nil, &dde_callback)
108
+ dde_callback ||= default_callback
28
109
  __start_service( name, init_flags, &dde_callback )
29
110
  end
30
111
 
data/lib/dde/xl_table.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  module DDE
2
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
-
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
9
 
10
10
  # XLTable class represents a single chunk of DDE data formatted as an Excel table
11
11
  class XlTable
@@ -32,21 +32,26 @@ module DDE
32
32
  TDT_TABLE => 'TDT_TABLE'
33
33
  }
34
34
 
35
- attr_accessor :topic_item # topic_item
35
+ attr_accessor :topic, # topic prefix
36
+ :time, # time spent parsing last transaction data
37
+ :total_time, # total time spent parsing data
38
+ :num_trans # number of data transactions
36
39
 
37
40
  def initialize
38
- @table_data = [] # Array contains Arrays of Strings
41
+ @table = [] # Array contains Arrays of Strings
39
42
  @col = 0
40
43
  @row = 0
44
+ @total_time = 0
45
+ @total_records = 0
41
46
  # omitting separators for now
42
47
  end
43
48
 
44
49
  # tests if table data is empty or contains data in inconsistent state
45
50
  def empty?
46
- @table_data.empty? ||
51
+ @table.empty? ||
47
52
  @row == 0 || @col == 0 ||
48
- @row != @table_data.size ||
49
- @col != @table_data.first.size # assumes first element is also an Array
53
+ @row != @table.size ||
54
+ @col != @table.first.size # assumes first element is also an Array
50
55
  end
51
56
 
52
57
  def data?;
@@ -55,118 +60,111 @@ module DDE
55
60
 
56
61
  def draw
57
62
  return false if empty?
58
-
63
+ Encoding.default_external = 'cp866'
59
64
  # omitting separator gymnastics for now
60
65
  cout "-----\n"
61
- @table_data.each{|row| row.each {|col| cout col, " "}; cout "\n"}
66
+ @table.each{|row| cout @topic; row.each {|col| cout " #{col}"}; cout "\n"}
62
67
  end
63
68
 
64
69
  def get_data(dde_handle)
65
- # conv = DDE::Conv.new # Union for data conversion
70
+ start = Time.now
66
71
 
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
72
+ @offset = 0
73
+ @pos = 0 #; @c=0; @r=0
70
74
 
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
75
+ @data, size = dde_get_data(dde_handle)
76
+ return nil unless @data && # DDE data is present at given dde_handle
77
+ read_int == TDT_TABLE && # and, first data block is tdtTable
78
+ read_int == 4 # and, its length is 4 bytes
74
79
 
75
- # Make sure cb == 4
76
- return nil unless data.get_int16(offset) == 4 # raise 'TDT_TABLE data length wrong'
77
- offset += 2
80
+ @row = read_int
81
+ @col = read_int
82
+ return nil unless @row != 0 && @col != 0 # Make sure nonzero row and col
78
83
 
79
- @row = data.get_int16(offset)
80
- @col = data.get_int16(offset+2)
81
- offset += 4
82
- #p "row, col", @row, @col
84
+ @table = Array.new(@row){||Array.new}
83
85
 
84
- # Make sure nonzero row and col
85
- return nil if @row == 0 || @col == 0 # raise 'col or row zero in TDT_TABLE'
86
+ while @offset < @data.size
87
+ type = read_int # Next data field(s) type
88
+ byte_size = read_int # Next data field(s) length in bytes
86
89
 
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}"
90
+ #p "type #{TDT_TYPES[type]}, cb #{byte_size}, row #{@pos/@col}, col #{@pos%@col}"
97
91
  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
92
+ when TDT_STRING # Strings, length byte followed by chars, no zero termination
93
+ field_end = @offset + byte_size
94
+ while @offset < field_end do
95
+ length = read_char
96
+ self.table = @data.get_bytes(@offset, length) #read_bytes(length)#.force_encoding('CP1251').encode('CP866')
97
+ @offset += length
120
98
  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
99
+ when TDT_FLOAT # Float, 8 bytes (used to represent Integers too in Quik!)
100
+ (byte_size/8).times do
101
+ # self.table = read_double
102
+ # end
103
+ float_or_int = @data.get_float64(@offset)
104
+ @offset += 8
105
+ int = float_or_int.round
106
+ self.table = float_or_int == int ? int : float_or_int
163
107
  end
108
+ when TDT_BLANK # Number of blank cells, 2 bytes
109
+ (byte_size/2).times { read_int.times { self.table = "" } }
110
+ when TDT_SKIP # Number of cells to skip, 2 bytes - in Quik, it means that these cells contain 0
111
+ (byte_size/2).times { read_int.times { self.table = 0 } }
112
+ when TDT_INT # Int, 2 bytes
113
+ (byte_size/2).times { self.table = read_int }
114
+ when TDT_BOOL # Bool, 2 bytes 0/1
115
+ (byte_size/2).times { self.table = read_int == 0 }
116
+ when TDT_ERROR # Error enum, 2 bytes
117
+ (byte_size/2).times { self.table = "Error:#{read_int}" }
164
118
  else
119
+ cout "Type: #{type}, #{TDT_TYPES[type]}"
165
120
  return nil
166
121
  end
167
122
  end
168
- #TODO: free FFI::Pointer ? delete []data; // Free memory
123
+ #TODO: free FFI::Pointer ? delete []data; // Free memory
124
+ @time = Time.now - start
125
+ @total_time += @time
126
+ @total_records += @row
169
127
  true # Data acquisition successful
170
128
  end
129
+
130
+ def timer
131
+ cout "Last: #{@row} in #{@time} s(#{@time/@row} s/rec), total: #{@total_records} in #{
132
+ @total_time} s(#{@total_time/@total_records} s/rec)\n"
133
+ end
134
+
135
+ def read_char
136
+ @offset += 1
137
+ @data.get_int8(@offset-1)
138
+ end
139
+
140
+ def read_int
141
+ @offset += 2
142
+ @data.get_int16(@offset-2)
143
+ end
144
+
145
+ def read_double
146
+ @offset += 8
147
+ @data.get_float64(@offset-8)
148
+ end
149
+
150
+ def read_bytes(length=1)
151
+ @offset += length
152
+ @data.get_bytes(@offset-length, length)
153
+ end
154
+
155
+ def table=(value)
156
+ # @table[@r][@c] = value
157
+ # todo: Add code for adding value to data row here (pack?)
158
+ # @c+=1
159
+ # if @c == @col
160
+ # @c =0
161
+ # @r+=1
162
+ # end
163
+ # todo: Add code for (sync!) publishing of assembled data row here (bunny? rosetta_queue?)
164
+
165
+ @table[@pos/@col][@pos%@col] = value
166
+ @pos += 1
167
+ end
168
+
171
169
  end
172
170
  end
@@ -0,0 +1,85 @@
1
+ module DDETest
2
+ shared_examples_for "DDE App" do
3
+ before(:each ){ @app = described_class.new }
4
+ after(:each ){ @app.stop_dde if @app.dde_active?}
5
+
6
+ it 'starts with nil id and flags if no arguments given' do
7
+ @app.id.should == nil
8
+ @app.init_flags.should == nil
9
+ @app.dde_active?.should == false
10
+ end unless described_class == DDE::Monitor
11
+
12
+ it 'starts DDE (initializes as STANDARD DDE app) with given callback block' do
13
+ @app = described_class.new {|*args|}
14
+ @app.id.should be_an Integer
15
+ @app.id.should_not == 0
16
+ @app.init_flags.should == APPCLASS_STANDARD
17
+ @app.dde_active?.should == true
18
+ end unless described_class == DDE::Monitor
19
+
20
+ describe '#start_dde' do
21
+ it 'starts DDE with callback and default init_flags' do
22
+ res = @app.start_dde {|*args|}
23
+ res.should be_true
24
+ @app.id.should be_an Integer
25
+ @app.id.should_not == 0
26
+ @app.init_flags.should == APPCLASS_STANDARD
27
+ @app.dde_active?.should == true
28
+ end unless described_class == DDE::Monitor
29
+
30
+ it 'returns self if success (allows method chain)' do
31
+ @app.start_dde {|*args|}.should == @app
32
+ end
33
+
34
+ it 'starts DDE with callback and given init_flags' do
35
+ res = @app.start_dde( APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS ){|*args|}
36
+ res.should be_true
37
+ @app.id.should be_an Integer
38
+ @app.id.should_not == 0
39
+ @app.init_flags.should == APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS
40
+ @app.dde_active?.should == true
41
+ end
42
+
43
+ it 'raises InitError if no callback was given' do
44
+ lambda{ @app.start_dde}.should raise_error DDE::Errors::InitError
45
+ end
46
+
47
+ it 'reinitializes with new flags and callback if it was already initialized' do
48
+ @app.start_dde {|*args| 1}
49
+ old_id = @app.id
50
+ res = @app.start_dde( APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS ){|*args| 2}
51
+ res.should be_true
52
+ @app.id.should == old_id
53
+ @app.init_flags.should == APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS
54
+ @app.dde_active?.should == true
55
+ end
56
+ end
57
+
58
+ describe '#stop_dde' do
59
+ it 'stops DDE that was active' do
60
+ @app.start_dde {|*args| 1}
61
+
62
+ @app.stop_dde
63
+ @app.id.should == nil
64
+ @app.dde_active?.should == false
65
+ end
66
+
67
+ it 'preserves init_flags after DDE is stopped (for reinitialization)' do
68
+ @app.start_dde(APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS) {|*args| 1}
69
+
70
+ @app.stop_dde
71
+ @app.init_flags.should == APPCLASS_STANDARD | CBF_FAIL_CONNECTIONS
72
+ end
73
+
74
+ it 'returns self if success (allows method chain)' do
75
+ @app.start_dde{|*args|}.stop_dde.should == @app
76
+ end
77
+
78
+ it 'raises InitError if dde was not active first' do
79
+ lambda{ @app.stop_dde}.should raise_error DDE::Errors::InitError
80
+ end
81
+ end unless described_class == DDE::Monitor
82
+
83
+
84
+ end
85
+ end
data/spec/dde/app_spec.rb CHANGED
@@ -1,85 +1,9 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require File.expand_path(File.dirname(__FILE__) + '/app_shared')
2
3
 
3
4
  module DDETest
4
5
  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
6
+ it_should_behave_like "DDE App"
83
7
 
84
8
  end
85
9
  end