dde 0.2.2 → 0.2.8

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