rtext 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rtext/service.rb CHANGED
@@ -1,260 +1,264 @@
1
- require 'socket'
2
- require 'rtext/context_builder'
3
- require 'rtext/message_helper'
4
- require 'rtext/link_detector'
5
-
6
- # optimization: garbage collect while service is idle
7
-
8
- module RText
9
-
10
- class Service
11
- include RText::MessageHelper
12
-
13
- PortRangeStart = 9001
14
- PortRangeEnd = 9100
15
-
16
- FlushInterval = 1
17
- ProtocolVersion = 1
18
-
19
- # Creates an RText backend service. Options:
20
- #
21
- # :timeout
22
- # idle time in seconds after which the service will terminate itself
23
- #
24
- # :logger
25
- # a logger object on which the service will write its logging output
26
- #
27
- # :on_startup:
28
- # a Proc which is called right after the service has started up
29
- # can be used to output version information
30
- #
31
- def initialize(service_provider, options={})
32
- @service_provider = service_provider
33
- @timeout = options[:timeout] || 60
34
- @logger = options[:logger]
35
- @on_startup = options[:on_startup]
36
- end
37
-
38
- def run
39
- server = create_server
40
- puts "RText service, listening on port #{server.addr[1]}"
41
- @on_startup.call if @on_startup
42
- $stdout.flush
43
-
44
- last_access_time = Time.now
45
- last_flush_time = Time.now
46
- @stop_requested = false
47
- sockets = []
48
- request_data = {}
49
- while !@stop_requested
50
- begin
51
- sock = server.accept_nonblock
52
- sock.sync = true
53
- sockets << sock
54
- @logger.info "accepted connection" if @logger
55
- rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR, Errno::EWOULDBLOCK
56
- rescue Exception => e
57
- @logger.warn "unexpected exception during socket accept: #{e.class}"
58
- end
59
- sockets.dup.each do |sock|
60
- data = nil
61
- begin
62
- data = sock.read_nonblock(100000)
63
- rescue Errno::EWOULDBLOCK
64
- rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
65
- sock.close
66
- request_data[sock] = nil
67
- sockets.delete(sock)
68
- rescue Exception => e
69
- # catch Exception to make sure we don't crash due to unexpected exceptions
70
- @logger.warn "unexpected exception during socket read: #{e.class}"
71
- sock.close
72
- request_data[sock] = nil
73
- sockets.delete(sock)
74
- end
75
- if data
76
- last_access_time = Time.now
77
- request_data[sock] ||= ""
78
- request_data[sock].concat(data)
79
- while obj = extract_message(request_data[sock])
80
- message_received(sock, obj)
81
- end
82
- end
83
- end
84
- IO.select([server] + sockets, [], [], 1)
85
- if Time.now > last_access_time + @timeout
86
- @logger.info("RText service, stopping now (timeout)") if @logger
87
- break
88
- end
89
- if Time.now > last_flush_time + FlushInterval
90
- $stdout.flush
91
- last_flush_time = Time.now
92
- end
93
- end
94
- end
95
-
96
- def message_received(sock, obj)
97
- if check_request(obj)
98
- request_start = Time.now
99
- @logger.debug("request: "+obj.inspect) if @logger
100
- response = { "type" => "response", "invocation_id" => obj["invocation_id"] }
101
- case obj["command"]
102
- when "version"
103
- version(sock, obj, response)
104
- when "load_model"
105
- load_model(sock, obj, response)
106
- when "content_complete"
107
- content_complete(sock, obj, response)
108
- when "link_targets"
109
- link_targets(sock, obj, response)
110
- when "find_elements"
111
- find_elements(sock, obj, response)
112
- when "stop"
113
- @logger.info("RText service, stopping now (stop requested)") if @logger
114
- @stop_requested = true
115
- else
116
- @logger.warn("unknown command #{obj["command"]}") if @logger
117
- response["type"] = "unknown_command_error"
118
- response["command"] = obj["command"]
119
- end
120
- @logger.debug("response: "+truncate_response_for_debug_output(response).inspect) \
121
- if response && @logger
122
- send_response(sock, response)
123
- @logger.info("request complete (#{Time.now-request_start}s)")
124
- end
125
- end
126
-
127
- private
128
-
129
- def check_request(obj)
130
- if obj["type"] != "request"
131
- @logger.warn("received message is not a request") if @logger
132
- false
133
- elsif !obj["invocation_id"].is_a?(Integer)
134
- @logger.warn("invalid invocation id #{obj["invocation_id"]}") if @logger
135
- false
136
- else
137
- true
138
- end
139
- end
140
-
141
- def version(sock, request, response)
142
- response["version"] = ProtocolVersion
143
- end
144
-
145
- def load_model(sock, request, response)
146
- problems = @service_provider.get_problems(
147
- :on_progress => lambda do |frag, work_done, work_overall|
148
- work_overall = 1 if work_overall < 1
149
- work_done = work_overall if work_done > work_overall
150
- work_done = 0 if work_done < 0
151
- send_response(sock, {
152
- "type" => "progress",
153
- "invocation_id" => request["invocation_id"],
154
- "percentage" => work_done*100/work_overall
155
- })
156
- end)
157
- total = 0
158
- response["problems"] = problems.collect do |fp|
159
- { "file" => fp.file,
160
- "problems" => fp.problems.collect do |p|
161
- total += 1
162
- { "severity" => "error", "line" => p.line, "message" => p.message }
163
- end }
164
- end
165
- response["total_problems"] = total
166
- end
167
-
168
- InsertString = "insert"
169
- DisplayString = "display"
170
- DescriptionString = "desc"
171
-
172
- def content_complete(sock, request, response)
173
- # column numbers start at 1
174
- linepos = request["column"]-1
175
- lines = request["context"]
176
- version = request["version"].to_i
177
- lang = @service_provider.language
178
- response["options"] = []
179
- return unless lang
180
- context = ContextBuilder.build_context(lang, lines, linepos)
181
- @logger.debug("context element: #{lang.identifier_provider.call(context.element, nil, nil, nil)}") \
182
- if context && context.element && @logger
183
- options = @service_provider.get_completion_options(context, version)
184
- response["options"] = options.collect do |o|
185
- { InsertString => o.insert, DisplayString => o.display, DescriptionString => o.description }
186
- end
187
- end
188
-
189
- def link_targets(sock, request, response)
190
- # column numbers start at 1
191
- linepos = request["column"]
192
- lines = request["context"]
193
- lang = @service_provider.language
194
- response["targets"] = []
195
- return unless lang
196
- link_descriptor = RText::LinkDetector.new(lang).detect(lines, linepos)
197
- if link_descriptor
198
- response["begin_column"] = link_descriptor.scol
199
- response["end_column"] = link_descriptor.ecol
200
- targets = []
201
- @service_provider.get_link_targets(link_descriptor).each do |t|
202
- targets << { "file" => t.file, "line" => t.line, "display" => t.display_name }
203
- end
204
- response["targets"] = targets
205
- end
206
- end
207
-
208
- def find_elements(sock, request, response)
209
- pattern = request["search_pattern"]
210
- total = 0
211
- response["elements"] = @service_provider.get_open_element_choices(pattern).collect do |c|
212
- total += 1
213
- { "display" => c.display_name, "file" => c.file, "line" => c.line }
214
- end
215
- response["total_elements"] = total
216
- end
217
-
218
- def send_response(sock, response)
219
- if response
220
- begin
221
- sock.write(serialize_message(response))
222
- sock.flush
223
- # if there is an exception, the next read should shutdown the connection properly
224
- rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
225
- rescue Exception => e
226
- # catch Exception to make sure we don't crash due to unexpected exceptions
227
- @logger.warn "unexpected exception during socket write: #{e.class}"
228
- end
229
- end
230
- end
231
-
232
- def truncate_response_for_debug_output(response_hash)
233
- result = {}
234
- response_hash.each_pair do |k,v|
235
- if v.is_a?(Array) && v.size > 100
236
- result[k] = v[0..99] + ["<truncated>"]
237
- else
238
- result[k] = v
239
- end
240
- end
241
- result
242
- end
243
-
244
- def create_server
245
- port = PortRangeStart
246
- serv = nil
247
- begin
248
- serv = TCPServer.new("127.0.0.1", port)
249
- rescue Errno::EADDRINUSE, Errno::EAFNOSUPPORT, Errno::EACCES
250
- port += 1
251
- retry if port <= PortRangeEnd
252
- raise
253
- end
254
- serv
255
- end
256
-
257
- end
258
-
259
- end
260
-
1
+ require 'socket'
2
+ require 'rtext/context_builder'
3
+ require 'rtext/message_helper'
4
+ require 'rtext/link_detector'
5
+
6
+ # optimization: garbage collect while service is idle
7
+
8
+ module RText
9
+
10
+ class Service
11
+ include RText::MessageHelper
12
+
13
+ PortRangeStart = 9001
14
+ PortRangeEnd = 9100
15
+
16
+ FlushInterval = 1
17
+ ProtocolVersion = 1
18
+
19
+ # Creates an RText backend service. Options:
20
+ #
21
+ # :timeout
22
+ # idle time in seconds after which the service will terminate itself
23
+ #
24
+ # :logger
25
+ # a logger object on which the service will write its logging output
26
+ #
27
+ # :on_startup:
28
+ # a Proc which is called right after the service has started up
29
+ # can be used to output version information
30
+ #
31
+ def initialize(service_provider, options={})
32
+ @service_provider = service_provider
33
+ @timeout = options[:timeout] || 60
34
+ @logger = options[:logger]
35
+ @on_startup = options[:on_startup]
36
+ end
37
+
38
+ def run
39
+ server = create_server
40
+ puts "RText service, listening on port #{server.addr[1]}"
41
+ @on_startup.call if @on_startup
42
+ $stdout.flush
43
+
44
+ last_access_time = Time.now
45
+ last_flush_time = Time.now
46
+ @stop_requested = false
47
+ sockets = []
48
+ request_data = {}
49
+ while !@stop_requested
50
+ begin
51
+ sock = server.accept_nonblock
52
+ sock.sync = true
53
+ sockets << sock
54
+ @logger.info "accepted connection" if @logger
55
+ rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR, Errno::EWOULDBLOCK
56
+ rescue Exception => e
57
+ @logger.warn "unexpected exception during socket accept: #{e.class}"
58
+ end
59
+ sockets.dup.each do |sock|
60
+ data = nil
61
+ begin
62
+ data = sock.read_nonblock(100000)
63
+ rescue Errno::EWOULDBLOCK
64
+ rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
65
+ sock.close
66
+ request_data[sock] = nil
67
+ sockets.delete(sock)
68
+ rescue Exception => e
69
+ # catch Exception to make sure we don't crash due to unexpected exceptions
70
+ @logger.warn "unexpected exception during socket read: #{e.class}"
71
+ sock.close
72
+ request_data[sock] = nil
73
+ sockets.delete(sock)
74
+ end
75
+ if data
76
+ last_access_time = Time.now
77
+ request_data[sock] ||= ""
78
+ request_data[sock].concat(data)
79
+ while obj = extract_message(request_data[sock])
80
+ message_received(sock, obj)
81
+ end
82
+ end
83
+ end
84
+ IO.select([server] + sockets, [], [], 1)
85
+ if Time.now > last_access_time + @timeout
86
+ @logger.info("RText service, stopping now (timeout)") if @logger
87
+ break
88
+ end
89
+ if Time.now > last_flush_time + FlushInterval
90
+ $stdout.flush
91
+ last_flush_time = Time.now
92
+ end
93
+ end
94
+ end
95
+
96
+ def message_received(sock, obj)
97
+ if check_request(obj)
98
+ request_start = Time.now
99
+ @logger.debug("request: "+obj.inspect) if @logger
100
+ response = { "type" => "response", "invocation_id" => obj["invocation_id"] }
101
+ case obj["command"]
102
+ when "version"
103
+ version(sock, obj, response)
104
+ when "load_model"
105
+ load_model(sock, obj, response)
106
+ when "content_complete"
107
+ content_complete(sock, obj, response)
108
+ when "link_targets"
109
+ link_targets(sock, obj, response)
110
+ when "find_elements"
111
+ find_elements(sock, obj, response)
112
+ when "stop"
113
+ @logger.info("RText service, stopping now (stop requested)") if @logger
114
+ @stop_requested = true
115
+ else
116
+ @logger.warn("unknown command #{obj["command"]}") if @logger
117
+ response["type"] = "unknown_command_error"
118
+ response["command"] = obj["command"]
119
+ end
120
+ @logger.debug("response: "+truncate_response_for_debug_output(response).inspect) \
121
+ if response && @logger
122
+ send_response(sock, response)
123
+ @logger.info("request complete (#{Time.now-request_start}s)")
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def check_request(obj)
130
+ if obj["type"] != "request"
131
+ @logger.warn("received message is not a request") if @logger
132
+ false
133
+ elsif !obj["invocation_id"].is_a?(Integer)
134
+ @logger.warn("invalid invocation id #{obj["invocation_id"]}") if @logger
135
+ false
136
+ else
137
+ true
138
+ end
139
+ end
140
+
141
+ def version(sock, request, response)
142
+ response["version"] = ProtocolVersion
143
+ end
144
+
145
+ def load_model(sock, request, response)
146
+ problems = @service_provider.get_problems(
147
+ :on_progress => lambda do |frag, work_done, work_overall|
148
+ work_overall = 1 if work_overall < 1
149
+ work_done = work_overall if work_done > work_overall
150
+ work_done = 0 if work_done < 0
151
+ send_response(sock, {
152
+ "type" => "progress",
153
+ "invocation_id" => request["invocation_id"],
154
+ "percentage" => work_done*100/work_overall
155
+ })
156
+ end)
157
+ total = 0
158
+ response["problems"] = problems.collect do |fp|
159
+ { "file" => fp.file,
160
+ "problems" => fp.problems.collect do |p|
161
+ total += 1
162
+ { "severity" => "error", "line" => p.line, "message" => p.message }
163
+ end }
164
+ end
165
+ response["total_problems"] = total
166
+ end
167
+
168
+ InsertString = "insert"
169
+ DisplayString = "display"
170
+ DescriptionString = "desc"
171
+
172
+ def content_complete(sock, request, response)
173
+ # column numbers start at 1
174
+ linepos = request["column"]-1
175
+ lines = request["context"]
176
+ version = request["version"].to_i
177
+ lang = @service_provider.language
178
+ response["options"] = []
179
+ return unless lang
180
+ context = ContextBuilder.build_context(lang, lines, linepos)
181
+ @logger.debug("context element: #{lang.identifier_provider.call(context.element, nil, nil, nil)}") \
182
+ if context && context.element && @logger
183
+ if @service_provider.method(:get_completion_options).arity == 1
184
+ options = @service_provider.get_completion_options(context)
185
+ else
186
+ options = @service_provider.get_completion_options(context, version)
187
+ end
188
+ response["options"] = options.collect do |o|
189
+ { InsertString => o.insert, DisplayString => o.display, DescriptionString => o.description }
190
+ end
191
+ end
192
+
193
+ def link_targets(sock, request, response)
194
+ # column numbers start at 1
195
+ linepos = request["column"]
196
+ lines = request["context"]
197
+ lang = @service_provider.language
198
+ response["targets"] = []
199
+ return unless lang
200
+ link_descriptor = RText::LinkDetector.new(lang).detect(lines, linepos)
201
+ if link_descriptor
202
+ response["begin_column"] = link_descriptor.scol
203
+ response["end_column"] = link_descriptor.ecol
204
+ targets = []
205
+ @service_provider.get_link_targets(link_descriptor).each do |t|
206
+ targets << { "file" => t.file, "line" => t.line, "display" => t.display_name }
207
+ end
208
+ response["targets"] = targets
209
+ end
210
+ end
211
+
212
+ def find_elements(sock, request, response)
213
+ pattern = request["search_pattern"]
214
+ total = 0
215
+ response["elements"] = @service_provider.get_open_element_choices(pattern).collect do |c|
216
+ total += 1
217
+ { "display" => c.display_name, "file" => c.file, "line" => c.line }
218
+ end
219
+ response["total_elements"] = total
220
+ end
221
+
222
+ def send_response(sock, response)
223
+ if response
224
+ begin
225
+ sock.write(serialize_message(response))
226
+ sock.flush
227
+ # if there is an exception, the next read should shutdown the connection properly
228
+ rescue IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED
229
+ rescue Exception => e
230
+ # catch Exception to make sure we don't crash due to unexpected exceptions
231
+ @logger.warn "unexpected exception during socket write: #{e.class}"
232
+ end
233
+ end
234
+ end
235
+
236
+ def truncate_response_for_debug_output(response_hash)
237
+ result = {}
238
+ response_hash.each_pair do |k,v|
239
+ if v.is_a?(Array) && v.size > 100
240
+ result[k] = v[0..99] + ["<truncated>"]
241
+ else
242
+ result[k] = v
243
+ end
244
+ end
245
+ result
246
+ end
247
+
248
+ def create_server
249
+ port = PortRangeStart
250
+ serv = nil
251
+ begin
252
+ serv = TCPServer.new("127.0.0.1", port)
253
+ rescue Errno::EADDRINUSE, Errno::EAFNOSUPPORT, Errno::EACCES
254
+ port += 1
255
+ retry if port <= PortRangeEnd
256
+ raise
257
+ end
258
+ serv
259
+ end
260
+
261
+ end
262
+
263
+ end
264
+