puppet-editor-services 2.0.4
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +510 -0
- data/CODEOWNERS +2 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +54 -0
- data/Gemfile +53 -0
- data/LICENSE +201 -0
- data/README.md +308 -0
- data/Rakefile +185 -0
- data/bin/puppet-debugserver +8 -0
- data/bin/puppet-languageserver +7 -0
- data/bin/puppet-languageserver-sidecar +7 -0
- data/lib/dsp/dsp.rb +7 -0
- data/lib/dsp/dsp_base.rb +62 -0
- data/lib/dsp/dsp_protocol.rb +4619 -0
- data/lib/lsp/lsp.rb +10 -0
- data/lib/lsp/lsp_base.rb +63 -0
- data/lib/lsp/lsp_custom.rb +170 -0
- data/lib/lsp/lsp_enums.rb +143 -0
- data/lib/lsp/lsp_protocol.rb +2785 -0
- data/lib/lsp/lsp_protocol_callhierarchy.proposed.rb +239 -0
- data/lib/lsp/lsp_protocol_colorprovider.rb +100 -0
- data/lib/lsp/lsp_protocol_configuration.rb +82 -0
- data/lib/lsp/lsp_protocol_declaration.rb +73 -0
- data/lib/lsp/lsp_protocol_foldingrange.rb +129 -0
- data/lib/lsp/lsp_protocol_implementation.rb +75 -0
- data/lib/lsp/lsp_protocol_progress.rb +200 -0
- data/lib/lsp/lsp_protocol_selectionrange.rb +79 -0
- data/lib/lsp/lsp_protocol_sematictokens.proposed.rb +340 -0
- data/lib/lsp/lsp_protocol_typedefinition.rb +75 -0
- data/lib/lsp/lsp_protocol_workspacefolders.rb +174 -0
- data/lib/lsp/lsp_types.rb +1534 -0
- data/lib/puppet-debugserver/debug_session/break_points.rb +137 -0
- data/lib/puppet-debugserver/debug_session/flow_control.rb +161 -0
- data/lib/puppet-debugserver/debug_session/hook_handlers.rb +295 -0
- data/lib/puppet-debugserver/debug_session/puppet_session_run_mode.rb +66 -0
- data/lib/puppet-debugserver/debug_session/puppet_session_state.rb +122 -0
- data/lib/puppet-debugserver/hooks.rb +132 -0
- data/lib/puppet-debugserver/message_handler.rb +277 -0
- data/lib/puppet-debugserver/puppet_debug_session.rb +541 -0
- data/lib/puppet-debugserver/puppet_monkey_patches.rb +118 -0
- data/lib/puppet-languageserver/client_session_state.rb +119 -0
- data/lib/puppet-languageserver/crash_dump.rb +50 -0
- data/lib/puppet-languageserver/epp/validation_provider.rb +34 -0
- data/lib/puppet-languageserver/facter_helper.rb +25 -0
- data/lib/puppet-languageserver/global_queues/sidecar_queue.rb +205 -0
- data/lib/puppet-languageserver/global_queues/single_instance_queue.rb +126 -0
- data/lib/puppet-languageserver/global_queues/validation_queue.rb +102 -0
- data/lib/puppet-languageserver/global_queues.rb +16 -0
- data/lib/puppet-languageserver/manifest/completion_provider.rb +331 -0
- data/lib/puppet-languageserver/manifest/definition_provider.rb +99 -0
- data/lib/puppet-languageserver/manifest/document_symbol_provider.rb +228 -0
- data/lib/puppet-languageserver/manifest/folding_provider.rb +226 -0
- data/lib/puppet-languageserver/manifest/format_on_type_provider.rb +143 -0
- data/lib/puppet-languageserver/manifest/hover_provider.rb +221 -0
- data/lib/puppet-languageserver/manifest/signature_provider.rb +169 -0
- data/lib/puppet-languageserver/manifest/validation_provider.rb +127 -0
- data/lib/puppet-languageserver/message_handler.rb +462 -0
- data/lib/puppet-languageserver/providers.rb +18 -0
- data/lib/puppet-languageserver/puppet_helper.rb +108 -0
- data/lib/puppet-languageserver/puppet_lexer_helper.rb +55 -0
- data/lib/puppet-languageserver/puppet_monkey_patches.rb +39 -0
- data/lib/puppet-languageserver/puppet_parser_helper.rb +212 -0
- data/lib/puppet-languageserver/puppetfile/validation_provider.rb +185 -0
- data/lib/puppet-languageserver/server_capabilities.rb +48 -0
- data/lib/puppet-languageserver/session_state/document_store.rb +272 -0
- data/lib/puppet-languageserver/session_state/language_client.rb +239 -0
- data/lib/puppet-languageserver/session_state/object_cache.rb +162 -0
- data/lib/puppet-languageserver/sidecar_protocol.rb +532 -0
- data/lib/puppet-languageserver/uri_helper.rb +46 -0
- data/lib/puppet-languageserver-sidecar/cache/base.rb +36 -0
- data/lib/puppet-languageserver-sidecar/cache/filesystem.rb +111 -0
- data/lib/puppet-languageserver-sidecar/cache/null.rb +27 -0
- data/lib/puppet-languageserver-sidecar/facter_helper.rb +41 -0
- data/lib/puppet-languageserver-sidecar/puppet_environment_monkey_patches.rb +52 -0
- data/lib/puppet-languageserver-sidecar/puppet_helper.rb +281 -0
- data/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb +146 -0
- data/lib/puppet-languageserver-sidecar/puppet_monkey_patches.rb +9 -0
- data/lib/puppet-languageserver-sidecar/puppet_parser_helper.rb +77 -0
- data/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb +399 -0
- data/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb +16 -0
- data/lib/puppet-languageserver-sidecar/sidecar_protocol_extensions.rb +16 -0
- data/lib/puppet-languageserver-sidecar/workspace.rb +89 -0
- data/lib/puppet_debugserver.rb +164 -0
- data/lib/puppet_editor_services/connection/base.rb +62 -0
- data/lib/puppet_editor_services/connection/stdio.rb +25 -0
- data/lib/puppet_editor_services/connection/tcp.rb +34 -0
- data/lib/puppet_editor_services/handler/base.rb +16 -0
- data/lib/puppet_editor_services/handler/debug_adapter.rb +63 -0
- data/lib/puppet_editor_services/handler/json_rpc.rb +133 -0
- data/lib/puppet_editor_services/logging.rb +45 -0
- data/lib/puppet_editor_services/protocol/base.rb +27 -0
- data/lib/puppet_editor_services/protocol/debug_adapter.rb +135 -0
- data/lib/puppet_editor_services/protocol/debug_adapter_messages.rb +171 -0
- data/lib/puppet_editor_services/protocol/json_rpc.rb +241 -0
- data/lib/puppet_editor_services/protocol/json_rpc_messages.rb +200 -0
- data/lib/puppet_editor_services/server/base.rb +42 -0
- data/lib/puppet_editor_services/server/stdio.rb +85 -0
- data/lib/puppet_editor_services/server/tcp.rb +349 -0
- data/lib/puppet_editor_services/server.rb +15 -0
- data/lib/puppet_editor_services/version.rb +36 -0
- data/lib/puppet_editor_services.rb +8 -0
- data/lib/puppet_languageserver.rb +263 -0
- data/lib/puppet_languageserver_sidecar.rb +361 -0
- data/puppet-debugserver +11 -0
- data/puppet-editor-services.gemspec +29 -0
- data/puppet-languageserver +15 -0
- data/puppet-languageserver-sidecar +14 -0
- metadata +240 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'puppet_editor_services/logging'
|
|
5
|
+
require 'puppet_editor_services/protocol/debug_adapter_messages'
|
|
6
|
+
require 'puppet_editor_services/protocol/base'
|
|
7
|
+
|
|
8
|
+
module PuppetEditorServices
|
|
9
|
+
module Protocol
|
|
10
|
+
class DebugAdapter < ::PuppetEditorServices::Protocol::Base
|
|
11
|
+
KEY_TYPE = 'type'
|
|
12
|
+
|
|
13
|
+
def initialize(connection)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
@state = :data
|
|
17
|
+
@buffer = []
|
|
18
|
+
|
|
19
|
+
@request_sequence_id = 0
|
|
20
|
+
# @requests = {}
|
|
21
|
+
@request_seq_mutex = Mutex.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def extract_headers(raw_header)
|
|
25
|
+
header = {}
|
|
26
|
+
raw_header.split("\r\n").each do |item|
|
|
27
|
+
name, value = item.split(':', 2)
|
|
28
|
+
|
|
29
|
+
if name.casecmp('Content-Length').zero?
|
|
30
|
+
header['Content-Length'] = value.strip.to_i
|
|
31
|
+
elsif name.casecmp('Content-Type').zero?
|
|
32
|
+
header['Content-Length'] = value.strip
|
|
33
|
+
else
|
|
34
|
+
raise("Unknown header #{name} in JSON message")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
header
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def receive_data(data)
|
|
41
|
+
# Inspired by https://github.com/PowerShell/PowerShellEditorServices/blob/dba65155c38d3d9eeffae5f0358b5a3ad0215fac/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs
|
|
42
|
+
return if data.empty?
|
|
43
|
+
return if @state == :ignore
|
|
44
|
+
|
|
45
|
+
# TODO: Thread/Atomic safe? probably not
|
|
46
|
+
@buffer += data.bytes.to_a
|
|
47
|
+
|
|
48
|
+
while @buffer.length > 4
|
|
49
|
+
# Check if we have enough data for the headers
|
|
50
|
+
# Need to find the first instance of '\r\n\r\n'
|
|
51
|
+
offset = 0
|
|
52
|
+
while offset < @buffer.length - 4
|
|
53
|
+
break if @buffer[offset] == 13 && @buffer[offset + 1] == 10 && @buffer[offset + 2] == 13 && @buffer[offset + 3] == 10
|
|
54
|
+
|
|
55
|
+
offset += 1
|
|
56
|
+
end
|
|
57
|
+
return unless offset < @buffer.length - 4
|
|
58
|
+
|
|
59
|
+
# Extract the headers
|
|
60
|
+
raw_header = @buffer.slice(0, offset).pack('C*').force_encoding('ASCII') # Note the headers are always ASCII encoded
|
|
61
|
+
headers = extract_headers(raw_header)
|
|
62
|
+
raise('Missing Content-Length header') if headers['Content-Length'].nil?
|
|
63
|
+
|
|
64
|
+
# Now we have the headers and the content length, do we have enough data now
|
|
65
|
+
minimum_buf_length = offset + 3 + headers['Content-Length'] + 1 # Need to add one as we're converting from offset (zero based) to length (1 based) arrays
|
|
66
|
+
return if @buffer.length < minimum_buf_length
|
|
67
|
+
|
|
68
|
+
# Extract the message content
|
|
69
|
+
content = @buffer.slice(offset + 3 + 1, headers['Content-Length']).pack('C*').force_encoding('utf-8') # TODO: default is utf-8. Need to enode based on Content-Type
|
|
70
|
+
# Purge the buffer
|
|
71
|
+
@buffer = @buffer.slice(minimum_buf_length, @buffer.length - minimum_buf_length)
|
|
72
|
+
@buffer = [] if @buffer.nil?
|
|
73
|
+
|
|
74
|
+
PuppetEditorServices.log_message(:debug, "--- INBOUND\n#{content}\n---")
|
|
75
|
+
receive_json_message_as_string(content)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def send_json_string(string)
|
|
80
|
+
PuppetEditorServices.log_message(:debug, "--- OUTBOUND\n#{string}\n---")
|
|
81
|
+
|
|
82
|
+
size = string.bytesize if string.respond_to?(:bytesize)
|
|
83
|
+
connection.send_data "Content-Length: #{size}\r\n\r\n" + string
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def encode_and_send(object)
|
|
87
|
+
# Inject the sequence ID.
|
|
88
|
+
raise "#{object.class} is not a PuppetEditorServices::Protocol::DebugAdapterMessages::ProtocolMessage" unless object.is_a?(PuppetEditorServices::Protocol::DebugAdapterMessages::ProtocolMessage)
|
|
89
|
+
|
|
90
|
+
object.seq = next_sequence_id!
|
|
91
|
+
send_json_string(::JSON.generate(object))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Seperate method so async JSON processing can be supported.
|
|
95
|
+
def receive_json_message_as_string(content)
|
|
96
|
+
json_obj = ::JSON.parse(content)
|
|
97
|
+
return receive_json_message_as_hash(json_obj) if json_obj.is_a?(Hash)
|
|
98
|
+
return unless json_obj.is_a?(Array)
|
|
99
|
+
|
|
100
|
+
# Batch: multiple requests/notifications in an array.
|
|
101
|
+
# NOTE: Not implemented as it doesn't make sense using JSON RPC over pure TCP / UnixSocket.
|
|
102
|
+
|
|
103
|
+
PuppetEditorServices.log_message(:error, 'Batch request received but not implemented')
|
|
104
|
+
send_json_string BATCH_NOT_SUPPORTED_RESPONSE
|
|
105
|
+
|
|
106
|
+
connection.close_after_writing
|
|
107
|
+
@state = :ignore
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def receive_json_message_as_hash(json_obj)
|
|
111
|
+
# There's no need to convert it to an object quite yet
|
|
112
|
+
# Need to validate that this is indeed a valid message
|
|
113
|
+
unless json_obj[KEY_TYPE] == 'request'
|
|
114
|
+
PuppetEditorServices.log_message(:error, "Unknown protocol message type #{json_obj[KEY_TYPE]}")
|
|
115
|
+
return false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
handler.handle(PuppetEditorServices::Protocol::DebugAdapterMessages::Request.new(json_obj))
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def next_sequence_id!
|
|
125
|
+
value = nil
|
|
126
|
+
@request_seq_mutex.synchronize do
|
|
127
|
+
value = @request_sequence_id
|
|
128
|
+
# TODO: Do we care about integer overflow? Probably not
|
|
129
|
+
@request_sequence_id += 1
|
|
130
|
+
end
|
|
131
|
+
value
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PuppetEditorServices
|
|
4
|
+
module Protocol
|
|
5
|
+
module DebugAdapterMessages
|
|
6
|
+
# Protocol message primitives
|
|
7
|
+
# interface ProtocolMessage {
|
|
8
|
+
# /** Sequence number. */
|
|
9
|
+
# seq: number;
|
|
10
|
+
# /** Message type.
|
|
11
|
+
# Values: 'request', 'response', 'event', etc.
|
|
12
|
+
# */
|
|
13
|
+
# type: string;
|
|
14
|
+
# }
|
|
15
|
+
class ProtocolMessage
|
|
16
|
+
attr_accessor :seq, :type # type: number # type: string
|
|
17
|
+
|
|
18
|
+
def initialize(initial_hash = nil)
|
|
19
|
+
from_h!(initial_hash)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_json(*options)
|
|
23
|
+
to_h.to_json(options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
'seq' => seq,
|
|
29
|
+
'type' => type
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def from_h!(value)
|
|
34
|
+
value = {} if value.nil?
|
|
35
|
+
self.seq = value['seq']
|
|
36
|
+
self.type = value['type']
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# interface Request extends ProtocolMessage {
|
|
42
|
+
# /** The command to execute. */
|
|
43
|
+
# command: string;
|
|
44
|
+
# /** Object containing arguments for the command. */
|
|
45
|
+
# arguments?: any;
|
|
46
|
+
# }
|
|
47
|
+
class Request < ProtocolMessage
|
|
48
|
+
attr_accessor :command, :arguments # type: string # type: any
|
|
49
|
+
|
|
50
|
+
def initialize(initial_hash = nil)
|
|
51
|
+
super
|
|
52
|
+
self.type = 'request'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def from_h!(value)
|
|
56
|
+
value = {} if value.nil?
|
|
57
|
+
super
|
|
58
|
+
self.command = value['command']
|
|
59
|
+
self.arguments = value['arguments']
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_h
|
|
64
|
+
super.tap do |hash|
|
|
65
|
+
hash['command'] = command
|
|
66
|
+
hash['arguments'] = arguments unless arguments.nil?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# interface Event extends ProtocolMessage {
|
|
72
|
+
# /** Type of event. */
|
|
73
|
+
# event: string;
|
|
74
|
+
# /** Event-specific information. */
|
|
75
|
+
# body?: any;
|
|
76
|
+
# }
|
|
77
|
+
class Event < ProtocolMessage
|
|
78
|
+
attr_accessor :event, :body # type: string # type: any
|
|
79
|
+
|
|
80
|
+
def initialize(initial_hash = nil)
|
|
81
|
+
super
|
|
82
|
+
self.type = 'event'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def from_h!(value)
|
|
86
|
+
value = {} if value.nil?
|
|
87
|
+
super
|
|
88
|
+
self.event = value['event']
|
|
89
|
+
self.body = value['body']
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def to_h
|
|
94
|
+
super.tap do |hash|
|
|
95
|
+
hash['event'] = event
|
|
96
|
+
hash['body'] = body unless body.nil?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# interface Response extends ProtocolMessage {
|
|
102
|
+
# /** Sequence number of the corresponding request. */
|
|
103
|
+
# request_seq: number;
|
|
104
|
+
# /** Outcome of the request. */
|
|
105
|
+
# success: boolean;
|
|
106
|
+
# /** The command requested. */
|
|
107
|
+
# command: string;
|
|
108
|
+
# /** Contains error message if success == false. */
|
|
109
|
+
# message?: string;
|
|
110
|
+
# /** Contains request result if success is true and optional error details if success is false. */
|
|
111
|
+
# body?: any;
|
|
112
|
+
# }
|
|
113
|
+
class Response < ProtocolMessage
|
|
114
|
+
attr_accessor :request_seq, :success, :command, :message, :body # type: number # type: boolean # type: string # type: string # type: any
|
|
115
|
+
|
|
116
|
+
def initialize(initial_hash = nil)
|
|
117
|
+
super
|
|
118
|
+
from_h!(initial_hash) unless initial_hash.nil?
|
|
119
|
+
self.type = 'response'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def from_h!(value)
|
|
123
|
+
value = {} if value.nil?
|
|
124
|
+
super
|
|
125
|
+
self.request_seq = value['request_seq']
|
|
126
|
+
self.success = value['success']
|
|
127
|
+
self.command = value['command']
|
|
128
|
+
self.message = value['message']
|
|
129
|
+
self.body = value['body']
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def to_h
|
|
134
|
+
super.tap do |hash|
|
|
135
|
+
hash['request_seq'] = request_seq
|
|
136
|
+
hash['success'] = success
|
|
137
|
+
hash['command'] = command
|
|
138
|
+
hash['message'] = message unless message.nil?
|
|
139
|
+
hash['body'] = body unless body.nil?
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Static message generators
|
|
145
|
+
def self.reply_error(request, message = nil, message_object = nil)
|
|
146
|
+
Response.new(
|
|
147
|
+
'request_seq' => request.seq,
|
|
148
|
+
'command' => request.command,
|
|
149
|
+
'success' => false
|
|
150
|
+
).tap do |resp|
|
|
151
|
+
resp.message = message unless message.nil?
|
|
152
|
+
resp.body = { 'error' => message_object } unless message_object.nil?
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.reply_success(request, body_content = nil)
|
|
157
|
+
Response.new(
|
|
158
|
+
'request_seq' => request.seq,
|
|
159
|
+
'command' => request.command,
|
|
160
|
+
'success' => true
|
|
161
|
+
).tap { |resp| resp.body = body_content unless body_content.nil? }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.new_event(event_name, body_content = nil)
|
|
165
|
+
Event.new(
|
|
166
|
+
'event' => event_name
|
|
167
|
+
).tap { |resp| resp.body = body_content unless body_content.nil? }
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'puppet_editor_services/logging'
|
|
5
|
+
require 'puppet_editor_services/protocol/json_rpc_messages'
|
|
6
|
+
require 'puppet_editor_services/protocol/base'
|
|
7
|
+
|
|
8
|
+
module PuppetEditorServices
|
|
9
|
+
module Protocol
|
|
10
|
+
class JsonRPC < ::PuppetEditorServices::Protocol::Base
|
|
11
|
+
CODE_INVALID_JSON = -32_700
|
|
12
|
+
MSG_INVALID_JSON = 'invalid JSON'
|
|
13
|
+
|
|
14
|
+
CODE_INVALID_REQUEST = -32_600
|
|
15
|
+
MSG_INVALID_REQ_JSONRPC = "invalid request: doesn't include \"jsonrpc\": \"2.0\""
|
|
16
|
+
MSG_INVALID_REQ_ID = 'invalid request: wrong id'
|
|
17
|
+
MSG_INVALID_REQ_METHOD = 'invalid request: wrong method'
|
|
18
|
+
MSG_INVALID_REQ_PARAMS = 'invalid request: wrong params'
|
|
19
|
+
|
|
20
|
+
CODE_METHOD_NOT_FOUND = -32_601
|
|
21
|
+
MSG_METHOD_NOT_FOUND = 'method not found'
|
|
22
|
+
|
|
23
|
+
CODE_INVALID_PARAMS = -32_602
|
|
24
|
+
MSG_INVALID_PARAMS = 'invalid parameter(s)'
|
|
25
|
+
|
|
26
|
+
CODE_INTERNAL_ERROR = -32_603
|
|
27
|
+
MSG_INTERNAL_ERROR = 'internal error'
|
|
28
|
+
|
|
29
|
+
PARSING_ERROR_RESPONSE = '{"jsonrpc":"2.0","id":null,"error":{' \
|
|
30
|
+
"\"code\":#{CODE_INVALID_JSON}," \
|
|
31
|
+
"\"message\":\"#{MSG_INVALID_JSON}\"}}"
|
|
32
|
+
|
|
33
|
+
BATCH_NOT_SUPPORTED_RESPONSE = '{"jsonrpc":"2.0","id":null,"error":{' \
|
|
34
|
+
'"code":-32099,' \
|
|
35
|
+
'"message":"batch mode not implemented"}}'
|
|
36
|
+
|
|
37
|
+
KEY_JSONRPC = 'jsonrpc'
|
|
38
|
+
JSONRPC_VERSION = '2.0'
|
|
39
|
+
KEY_ID = 'id'
|
|
40
|
+
KEY_METHOD = 'method'
|
|
41
|
+
KEY_PARAMS = 'params'
|
|
42
|
+
KEY_RESULT = 'result'
|
|
43
|
+
KEY_ERROR = 'error'
|
|
44
|
+
KEY_CODE = 'code'
|
|
45
|
+
KEY_MESSAGE = 'message'
|
|
46
|
+
|
|
47
|
+
def initialize(connection)
|
|
48
|
+
super
|
|
49
|
+
|
|
50
|
+
@state = :data
|
|
51
|
+
@buffer = []
|
|
52
|
+
|
|
53
|
+
@request_sequence_id = 0
|
|
54
|
+
@requests = {}
|
|
55
|
+
@request_mutex = Mutex.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_headers(raw_header)
|
|
59
|
+
header = {}
|
|
60
|
+
raw_header.split("\r\n").each do |item|
|
|
61
|
+
name, value = item.split(':', 2)
|
|
62
|
+
|
|
63
|
+
if name.casecmp('Content-Length').zero?
|
|
64
|
+
header['Content-Length'] = value.strip.to_i
|
|
65
|
+
elsif name.casecmp('Content-Type').zero?
|
|
66
|
+
header['Content-Length'] = value.strip
|
|
67
|
+
else
|
|
68
|
+
raise("Unknown header #{name} in JSON message")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
header
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def receive_data(data)
|
|
75
|
+
# Inspired by https://github.com/PowerShell/PowerShellEditorServices/blob/dba65155c38d3d9eeffae5f0358b5a3ad0215fac/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs
|
|
76
|
+
return if data.empty?
|
|
77
|
+
return if @state == :ignore
|
|
78
|
+
|
|
79
|
+
# TODO: Thread/Atomic safe? probably not
|
|
80
|
+
@buffer += data.bytes.to_a
|
|
81
|
+
|
|
82
|
+
while @buffer.length > 4
|
|
83
|
+
# Check if we have enough data for the headers
|
|
84
|
+
# Need to find the first instance of '\r\n\r\n'
|
|
85
|
+
offset = 0
|
|
86
|
+
while offset < @buffer.length - 4
|
|
87
|
+
break if @buffer[offset] == 13 && @buffer[offset + 1] == 10 && @buffer[offset + 2] == 13 && @buffer[offset + 3] == 10
|
|
88
|
+
|
|
89
|
+
offset += 1
|
|
90
|
+
end
|
|
91
|
+
return unless offset < @buffer.length - 4
|
|
92
|
+
|
|
93
|
+
# Extract the headers
|
|
94
|
+
raw_header = @buffer.slice(0, offset).pack('C*').force_encoding('ASCII') # Note the headers are always ASCII encoded
|
|
95
|
+
headers = extract_headers(raw_header)
|
|
96
|
+
raise('Missing Content-Length header') if headers['Content-Length'].nil?
|
|
97
|
+
|
|
98
|
+
# Now we have the headers and the content length, do we have enough data now
|
|
99
|
+
minimum_buf_length = offset + 3 + headers['Content-Length'] + 1 # Need to add one as we're converting from offset (zero based) to length (1 based) arrays
|
|
100
|
+
return if @buffer.length < minimum_buf_length
|
|
101
|
+
|
|
102
|
+
# Extract the message content
|
|
103
|
+
content = @buffer.slice(offset + 3 + 1, headers['Content-Length']).pack('C*').force_encoding('utf-8') # TODO: default is utf-8. Need to enode based on Content-Type
|
|
104
|
+
# Purge the buffer
|
|
105
|
+
@buffer = @buffer.slice(minimum_buf_length, @buffer.length - minimum_buf_length)
|
|
106
|
+
@buffer = [] if @buffer.nil?
|
|
107
|
+
|
|
108
|
+
PuppetEditorServices.log_message(:debug, "--- INBOUND\n#{content}\n---")
|
|
109
|
+
receive_json_message_as_string(content)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def send_json_string(string)
|
|
114
|
+
PuppetEditorServices.log_message(:debug, "--- OUTBOUND\n#{string}\n---")
|
|
115
|
+
|
|
116
|
+
size = string.bytesize if string.respond_to?(:bytesize)
|
|
117
|
+
connection.send_data "Content-Length: #{size}\r\n\r\n" + string
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def encode_and_send(object)
|
|
121
|
+
send_json_string(::JSON.generate(object))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Seperate method so async JSON processing can be supported.
|
|
125
|
+
def receive_json_message_as_string(content)
|
|
126
|
+
json_obj = ::JSON.parse(content)
|
|
127
|
+
return receive_json_message_as_hash(json_obj) if json_obj.is_a?(Hash)
|
|
128
|
+
return unless json_obj.is_a?(Array)
|
|
129
|
+
|
|
130
|
+
# Batch: multiple requests/notifications in an array.
|
|
131
|
+
# NOTE: Not implemented as it doesn't make sense using JSON RPC over pure TCP / UnixSocket.
|
|
132
|
+
|
|
133
|
+
PuppetEditorServices.log_message(:error, 'Batch request received but not implemented')
|
|
134
|
+
send_json_string BATCH_NOT_SUPPORTED_RESPONSE
|
|
135
|
+
|
|
136
|
+
connection.close_after_writing
|
|
137
|
+
@state = :ignore
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def receive_json_message_as_hash(json_obj)
|
|
141
|
+
# There's no need to convert it to an object quite yet
|
|
142
|
+
# Need to validate that this is indeed a valid message
|
|
143
|
+
id = json_obj[KEY_ID]
|
|
144
|
+
unless json_obj[KEY_JSONRPC] == JSONRPC_VERSION
|
|
145
|
+
PuppetEditorServices.log_message(:error, 'Invalid JSON RPC version')
|
|
146
|
+
reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_JSONRPC
|
|
147
|
+
return false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Requests must have an ID and Method
|
|
151
|
+
is_request = json_obj.key?(KEY_ID) && json_obj.key?(KEY_METHOD)
|
|
152
|
+
# Notifications must have a Method but no ID
|
|
153
|
+
is_notification = json_obj.key?(KEY_METHOD) && !json_obj.key?(KEY_ID)
|
|
154
|
+
# Responses must have an ID, no Method but one of Result or Error
|
|
155
|
+
is_response = json_obj.key?(KEY_ID) && !json_obj.key?(KEY_METHOD) && (json_obj.key?(KEY_RESULT) || json_obj.key?(KEY_ERROR))
|
|
156
|
+
|
|
157
|
+
# The 'params' attribute must be a hash or an array
|
|
158
|
+
if (params = json_obj[KEY_PARAMS]) && !(params.is_a?(Array) || params.is_a?(Hash))
|
|
159
|
+
reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_PARAMS
|
|
160
|
+
return false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Requests and Responses must have an ID that is either a string or integer
|
|
164
|
+
if (is_request || is_response) && !(id.is_a?(String) || id.is_a?(Integer))
|
|
165
|
+
reply_error nil, CODE_INVALID_REQUEST, MSG_INVALID_REQ_ID
|
|
166
|
+
return false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Requests and Notifications must have a method
|
|
170
|
+
if (is_request || is_notification) && !((json_obj[KEY_METHOD]).is_a? String)
|
|
171
|
+
reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_METHOD
|
|
172
|
+
return false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Responses must have a matching request originating from this instance
|
|
176
|
+
# Otherwise ignore it
|
|
177
|
+
if is_response
|
|
178
|
+
original_request = client_request!(json_obj[KEY_ID])
|
|
179
|
+
return false if original_request.nil?
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if is_request
|
|
183
|
+
handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::RequestMessage.new(json_obj))
|
|
184
|
+
return true
|
|
185
|
+
elsif is_notification
|
|
186
|
+
handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::NotificationMessage.new(json_obj))
|
|
187
|
+
return true
|
|
188
|
+
elsif is_response
|
|
189
|
+
# Responses are special as they need the context of the original request
|
|
190
|
+
handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::ResponseMessage.new(json_obj), request: original_request)
|
|
191
|
+
return true
|
|
192
|
+
end
|
|
193
|
+
false
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def reply_error(id, code, message)
|
|
197
|
+
send_json_string ::PuppetEditorServices::Protocol::JsonRPCMessages.reply_error_by_id(id, code, message).to_json
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# region Server-to-Client request/response methods
|
|
201
|
+
def send_client_request(rpc_method, params)
|
|
202
|
+
request = ::PuppetEditorServices::Protocol::JsonRPCMessages.new_request(client_request_id!, rpc_method, params)
|
|
203
|
+
encode_and_send(request)
|
|
204
|
+
add_client_request(request)
|
|
205
|
+
request.id
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Thread-safe way to get a new request id
|
|
209
|
+
def client_request_id!
|
|
210
|
+
value = nil
|
|
211
|
+
@request_mutex.synchronize do
|
|
212
|
+
value = @request_sequence_id
|
|
213
|
+
@request_sequence_id += 1
|
|
214
|
+
end
|
|
215
|
+
value
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Stores the request so it can later be correlated with an
|
|
219
|
+
# incoming repsonse
|
|
220
|
+
def add_client_request(request)
|
|
221
|
+
@request_mutex.synchronize do
|
|
222
|
+
@requests[request.id] = request
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Retrieve the request to a client. Note that this removes it
|
|
227
|
+
# from the requests queue.
|
|
228
|
+
def client_request!(id)
|
|
229
|
+
value = nil
|
|
230
|
+
@request_mutex.synchronize do
|
|
231
|
+
unless @requests[id].nil?
|
|
232
|
+
value = @requests[id]
|
|
233
|
+
@requests.delete(id)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
value
|
|
237
|
+
end
|
|
238
|
+
# endregion
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|