rtext 0.9.0 → 0.9.1

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/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
+