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,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'puppet_editor_services/protocol/json_rpc'
|
|
4
|
+
|
|
5
|
+
module PuppetEditorServices
|
|
6
|
+
module Protocol
|
|
7
|
+
module JsonRPCMessages
|
|
8
|
+
# Protocol message primitives
|
|
9
|
+
class Message
|
|
10
|
+
attr_accessor :jsonrpc
|
|
11
|
+
|
|
12
|
+
def initialize(initial_hash = nil)
|
|
13
|
+
@jsonrpc = ::PuppetEditorServices::Protocol::JsonRPC::JSONRPC_VERSION
|
|
14
|
+
from_h!(initial_hash)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def from_h!(value)
|
|
18
|
+
return self if value.nil? || value.empty?
|
|
19
|
+
|
|
20
|
+
# jsonrpc is a little special. Don't override it with nil. This
|
|
21
|
+
# allows `.new.from_h!(..)` to use the default without knowing exactly
|
|
22
|
+
# what version is used.
|
|
23
|
+
self.jsonrpc = value['jsonrpc'] unless value['jsonrpc'].nil?
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_json(*options)
|
|
28
|
+
to_h.to_json(options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{
|
|
33
|
+
'jsonrpc' => jsonrpc
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# interface RequestMessage extends Message {
|
|
39
|
+
# /**
|
|
40
|
+
# * The request id.
|
|
41
|
+
# */
|
|
42
|
+
# id: number | string;
|
|
43
|
+
# /**
|
|
44
|
+
# * The method to be invoked.
|
|
45
|
+
# */
|
|
46
|
+
# method: string;
|
|
47
|
+
# /**
|
|
48
|
+
# * The method's params.
|
|
49
|
+
# */
|
|
50
|
+
# params?: Array<any> | object;
|
|
51
|
+
# }
|
|
52
|
+
class RequestMessage < Message
|
|
53
|
+
attr_accessor :id, :rpc_method, :params
|
|
54
|
+
|
|
55
|
+
def initialize(initial_hash = nil)
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def from_h!(value)
|
|
60
|
+
value = {} if value.nil?
|
|
61
|
+
super
|
|
62
|
+
self.id = value['id']
|
|
63
|
+
self.rpc_method = value['method']
|
|
64
|
+
self.params = value['params']
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_h
|
|
69
|
+
super.merge(
|
|
70
|
+
'id' => id,
|
|
71
|
+
'method' => rpc_method,
|
|
72
|
+
'params' => params
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# interface NotificationMessage extends Message {
|
|
78
|
+
# /**
|
|
79
|
+
# * The method to be invoked.
|
|
80
|
+
# */
|
|
81
|
+
# method: string;
|
|
82
|
+
|
|
83
|
+
# /**
|
|
84
|
+
# * The notification's params.
|
|
85
|
+
# */
|
|
86
|
+
# params?: Array<any> | object;
|
|
87
|
+
# }
|
|
88
|
+
class NotificationMessage < Message
|
|
89
|
+
attr_accessor :rpc_method, :params
|
|
90
|
+
|
|
91
|
+
def initialize(initial_hash = nil)
|
|
92
|
+
super
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def from_h!(value)
|
|
96
|
+
value = {} if value.nil?
|
|
97
|
+
super
|
|
98
|
+
self.rpc_method = value['method']
|
|
99
|
+
self.params = value['params']
|
|
100
|
+
self
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def to_h
|
|
104
|
+
hash = { 'method' => rpc_method }
|
|
105
|
+
hash['params'] = params unless params.nil?
|
|
106
|
+
super.merge(hash)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# interface ResponseMessage extends Message {
|
|
111
|
+
# /**
|
|
112
|
+
# * The request id.
|
|
113
|
+
# */
|
|
114
|
+
# id: number | string | null;
|
|
115
|
+
|
|
116
|
+
# /**
|
|
117
|
+
# * The result of a request. This member is REQUIRED on success.
|
|
118
|
+
# * This member MUST NOT exist if there was an error invoking the method.
|
|
119
|
+
# */
|
|
120
|
+
# result?: string | number | boolean | object | null;
|
|
121
|
+
|
|
122
|
+
# /**
|
|
123
|
+
# * The error object in case a request fails.
|
|
124
|
+
# */
|
|
125
|
+
# error?: ResponseError<any>;
|
|
126
|
+
# }
|
|
127
|
+
class ResponseMessage < Message
|
|
128
|
+
attr_accessor :id, :result, :error
|
|
129
|
+
# is_successful is special. It changes based on deserialising from hash or
|
|
130
|
+
# can be manually set. This affects serialisation
|
|
131
|
+
attr_accessor :is_successful
|
|
132
|
+
|
|
133
|
+
def initialize(initial_hash = nil)
|
|
134
|
+
super
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def from_h!(value)
|
|
138
|
+
value = {} if value.nil?
|
|
139
|
+
super
|
|
140
|
+
self.id = value['id']
|
|
141
|
+
self.result = value['result']
|
|
142
|
+
self.error = value['error']
|
|
143
|
+
self.is_successful = value.key?('result')
|
|
144
|
+
self
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def to_h
|
|
148
|
+
hash = { 'id' => id }
|
|
149
|
+
# Ref - https://www.jsonrpc.org/specification#response_object
|
|
150
|
+
if is_successful
|
|
151
|
+
# Succesful responses must ALWAYS have the result key, even if it's null.
|
|
152
|
+
hash['result'] = result
|
|
153
|
+
else
|
|
154
|
+
# Error responses must ALWAYS have the error key
|
|
155
|
+
# TODO: The RPC spec says error MUST be an Error object, not nil.
|
|
156
|
+
hash['error'] = error
|
|
157
|
+
end
|
|
158
|
+
super.merge(hash)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Static message generators
|
|
163
|
+
def self.reply_result(request, result)
|
|
164
|
+
ResponseMessage.new.from_h!('id' => request.id, 'result' => result)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.reply_error(request, code, message)
|
|
168
|
+
reply_error_by_id(request.id, code, message)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def self.reply_error_by_id(id, code, message)
|
|
172
|
+
# Note - Strictly speaking the error should be typed object, however as this hidden behind
|
|
173
|
+
# this method it's easier to just pass in a known hash construct
|
|
174
|
+
ResponseMessage.new.from_h!(
|
|
175
|
+
'id' => id,
|
|
176
|
+
'error' => {
|
|
177
|
+
'code' => code,
|
|
178
|
+
'message' => message
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.reply_method_not_found(request, message = nil)
|
|
184
|
+
reply_error(
|
|
185
|
+
request,
|
|
186
|
+
::PuppetEditorServices::Protocol::JsonRPC::CODE_METHOD_NOT_FOUND,
|
|
187
|
+
message || ::PuppetEditorServices::Protocol::JsonRPC::MSG_METHOD_NOT_FOUND
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def self.new_notification(method_name, params)
|
|
192
|
+
NotificationMessage.new.from_h!('method' => method_name, 'params' => params)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def self.new_request(id, method_name, params)
|
|
196
|
+
RequestMessage.new.from_h!('id' => id, 'method' => method_name, 'params' => params)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'puppet_editor_services/server'
|
|
4
|
+
require 'puppet_editor_services/connection/base'
|
|
5
|
+
require 'puppet_editor_services/protocol/base'
|
|
6
|
+
|
|
7
|
+
module PuppetEditorServices
|
|
8
|
+
module Server
|
|
9
|
+
class Base
|
|
10
|
+
attr_reader :server_options, :connection_options, :protocol_options, :handler_options
|
|
11
|
+
|
|
12
|
+
def name
|
|
13
|
+
'SRV'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(server_options, connection_options, protocol_options, handler_options)
|
|
17
|
+
@server_options = server_options.nil? ? {} : server_options.dup
|
|
18
|
+
@connection_options = connection_options.nil? ? {} : connection_options.dup
|
|
19
|
+
@protocol_options = protocol_options.nil? ? {} : protocol_options.dup
|
|
20
|
+
@handler_options = handler_options.nil? ? {} : handler_options.dup
|
|
21
|
+
|
|
22
|
+
@connection_options[:class] = PuppetEditorServices::Connection::Base if @connection_options[:class].nil?
|
|
23
|
+
@protocol_options[:class] = PuppetEditorServices::Protocol::Base if @protocol_options[:class].nil?
|
|
24
|
+
@handler_options[:class] = PuppetEditorServices::Handler::Base if @handler_options[:class].nil?
|
|
25
|
+
|
|
26
|
+
@server_options[:servicename] = 'LANGUAGE SERVER' if @server_options[:servicename].nil?
|
|
27
|
+
|
|
28
|
+
# Assumes there's only ONE active simpler server running at a time.
|
|
29
|
+
PuppetEditorServices::Server.current_server = self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns a client connection for a given connection_id
|
|
33
|
+
def connection(connection_id); end
|
|
34
|
+
|
|
35
|
+
def start; end
|
|
36
|
+
|
|
37
|
+
def log(message)
|
|
38
|
+
PuppetEditorServices.log_message(:debug, "#{name}: #{message}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'puppet_editor_services/logging'
|
|
4
|
+
require 'puppet_editor_services/server/base'
|
|
5
|
+
require 'puppet_editor_services/connection/stdio'
|
|
6
|
+
require 'puppet_editor_services/protocol/base'
|
|
7
|
+
|
|
8
|
+
module PuppetEditorServices
|
|
9
|
+
module Server
|
|
10
|
+
class Stdio < ::PuppetEditorServices::Server::Base
|
|
11
|
+
attr_accessor :exiting
|
|
12
|
+
|
|
13
|
+
def name
|
|
14
|
+
'STDIOSRV'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(server_options, protocol_options, handler_options)
|
|
18
|
+
super(server_options, {}, protocol_options, handler_options)
|
|
19
|
+
@exiting = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def start
|
|
23
|
+
# This is a little heavy handed but we need to suppress writes to STDOUT and STDERR
|
|
24
|
+
$VERBOSE = nil
|
|
25
|
+
# Some libraries use $stdout to write to the console. Suppress all of that too!
|
|
26
|
+
# Copy the existing $stdout variable and then reassign to NUL to suppress it
|
|
27
|
+
$editor_services_stdout = $stdout # rubocop:disable Style/GlobalVars We need this global var
|
|
28
|
+
$stdout = File.open(File::NULL, 'w')
|
|
29
|
+
|
|
30
|
+
$editor_services_stdout.sync = true # rubocop:disable Style/GlobalVars We need this global var
|
|
31
|
+
# Stop the stupid CRLF injection when on Windows
|
|
32
|
+
$editor_services_stdout.binmode unless $editor_services_stdout.binmode # rubocop:disable Style/GlobalVars We need this global var
|
|
33
|
+
|
|
34
|
+
@client_connection = PuppetEditorServices::Connection::Stdio.new(self)
|
|
35
|
+
|
|
36
|
+
log('Starting STDIO server...')
|
|
37
|
+
loop do
|
|
38
|
+
inbound_data = nil
|
|
39
|
+
read_from_pipe($stdin, 2) { |data| inbound_data = data }
|
|
40
|
+
break if @exiting
|
|
41
|
+
|
|
42
|
+
@client_connection.receive_data(inbound_data) unless inbound_data.nil?
|
|
43
|
+
break if @exiting
|
|
44
|
+
end
|
|
45
|
+
log('STDIO server stopped')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def stop
|
|
49
|
+
log('Stopping STDIO server...')
|
|
50
|
+
@exiting = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def connection(connection_id)
|
|
54
|
+
@client_connection unless @client_connection.nil? || @client_connection.id != connection_id
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def close_connection
|
|
58
|
+
stop
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def pipe_is_readable?(stream, timeout = 0.5)
|
|
62
|
+
read_ready = IO.select([stream], [], [], timeout)
|
|
63
|
+
read_ready && stream == read_ready[0][0]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def read_from_pipe(pipe, timeout = 0.1, &_block)
|
|
67
|
+
if pipe_is_readable?(pipe, timeout)
|
|
68
|
+
l = nil
|
|
69
|
+
begin
|
|
70
|
+
l = pipe.readpartial(4096)
|
|
71
|
+
rescue EOFError
|
|
72
|
+
log('Reading from pipe has reached End of File. Exiting STDIO server')
|
|
73
|
+
stop
|
|
74
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
75
|
+
# Any errors here should be swallowed because the pipe could be in any state
|
|
76
|
+
end
|
|
77
|
+
# since readpartial may return a nil at EOF, skip returning that value
|
|
78
|
+
# client_connected = true unless l.nil?
|
|
79
|
+
yield l unless l.nil?
|
|
80
|
+
end
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
# frozen_string_literal: true
|
|
7
|
+
|
|
8
|
+
require 'puppet_editor_services/logging'
|
|
9
|
+
require 'puppet_editor_services/server/base'
|
|
10
|
+
require 'puppet_editor_services/connection/tcp'
|
|
11
|
+
require 'puppet_editor_services/protocol/base'
|
|
12
|
+
|
|
13
|
+
# Based on code from
|
|
14
|
+
# http://stackoverflow.com/questions/29858113/unable-to-make-socket-accept-non-blocking-ruby-2-2
|
|
15
|
+
|
|
16
|
+
module PuppetEditorServices
|
|
17
|
+
module Server
|
|
18
|
+
class Tcp < ::PuppetEditorServices::Server::Base
|
|
19
|
+
class << self
|
|
20
|
+
attr_reader :io_locker, :events, :e_locker, :services, :s_locker, :io_connection_dic, :c_locker
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@io_locker = Mutex.new
|
|
24
|
+
@events = []
|
|
25
|
+
@e_locker = Mutex.new
|
|
26
|
+
@services = {}
|
|
27
|
+
@s_locker = Mutex.new
|
|
28
|
+
@io_connection_dic = {}
|
|
29
|
+
@c_locker = Mutex.new
|
|
30
|
+
|
|
31
|
+
def initialize(server_options, protocol_options, handler_options)
|
|
32
|
+
super(server_options, {}, protocol_options, handler_options)
|
|
33
|
+
|
|
34
|
+
add_service(server_options[:ipaddress], server_options[:port])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def name
|
|
38
|
+
'TCPSRV'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
####
|
|
42
|
+
# this code will be called when a socket recieves data.
|
|
43
|
+
# @api private
|
|
44
|
+
def get_data(io, connection_data)
|
|
45
|
+
data = io.recv_nonblock(1_048_576) # with maximum number of bytes to read at a time...
|
|
46
|
+
raise 'Received a 0byte payload' if data.length.zero?
|
|
47
|
+
|
|
48
|
+
# We're already in a callback so no need to invoke as a callback
|
|
49
|
+
connection_data[:handler].receive_data(data)
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
log("Closing socket due to error - #{e}\n#{e.backtrace}")
|
|
52
|
+
remove_connection(io)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#########
|
|
56
|
+
# main loop and activation code
|
|
57
|
+
#
|
|
58
|
+
# This will create a thread pool and set them running.
|
|
59
|
+
# @api public
|
|
60
|
+
def start
|
|
61
|
+
# prepare threads
|
|
62
|
+
max_threads = server_options[:max_threads] || 2
|
|
63
|
+
exit_flag = false
|
|
64
|
+
threads = []
|
|
65
|
+
thread_cycle = proc do
|
|
66
|
+
begin
|
|
67
|
+
io_review
|
|
68
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
69
|
+
# Swallow all errors
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
true while fire_event
|
|
73
|
+
end
|
|
74
|
+
max_threads.times { Thread.new { thread_cycle.call until exit_flag } }
|
|
75
|
+
|
|
76
|
+
log('Services running. Press ^C to stop')
|
|
77
|
+
|
|
78
|
+
# sleep until trap raises exception (cycling might cause the main thread to loose signals that might be caught inside rescue clauses)
|
|
79
|
+
kill_timer = server_options[:connection_timeout]
|
|
80
|
+
kill_timer = -1 if kill_timer.nil? || kill_timer < 1
|
|
81
|
+
log("Will stop the server in #{server_options[:connection_timeout]} seconds if no connection is made.") if kill_timer > 0
|
|
82
|
+
log('Will stop the server when client disconnects') if !server_options[:stop_on_client_exit].nil? && server_options[:stop_on_client_exit]
|
|
83
|
+
|
|
84
|
+
# Output to STDOUT. This is required by clients so it knows the server is now running
|
|
85
|
+
self.class.s_locker.synchronize do
|
|
86
|
+
self.class.services.each_value do |service|
|
|
87
|
+
$stdout.write("#{server_options[:servicename]} RUNNING #{service[:hostname]}:#{service[:port]}\n")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
$stdout.flush
|
|
91
|
+
|
|
92
|
+
loop do
|
|
93
|
+
begin
|
|
94
|
+
sleep(1)
|
|
95
|
+
# The kill_timer is used to stop the server if no clients have connected in X seconds
|
|
96
|
+
# a value of 0 or less will not timeout.
|
|
97
|
+
if kill_timer > 0
|
|
98
|
+
kill_timer -= 1
|
|
99
|
+
if kill_timer.zero?
|
|
100
|
+
connection_count = 0
|
|
101
|
+
self.class.c_locker.synchronize { connection_count = self.class.io_connection_dic.count }
|
|
102
|
+
if connection_count.zero?
|
|
103
|
+
log("No connection has been received in #{server_options[:connection_timeout]} seconds. Shutting down server.")
|
|
104
|
+
stop_services
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
109
|
+
# Swallow all errors
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
break if self.class.services.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# start shutdown.
|
|
116
|
+
exit_flag = true
|
|
117
|
+
log('Started shutdown process. Press ^C to force quit.')
|
|
118
|
+
# shut down listening sockets
|
|
119
|
+
stop_services
|
|
120
|
+
# disconnect active connections
|
|
121
|
+
stop_connections
|
|
122
|
+
# cycle down threads
|
|
123
|
+
log('Waiting for workers to cycle down')
|
|
124
|
+
threads.each { |t| t.join if t.alive? }
|
|
125
|
+
|
|
126
|
+
# rundown any active events
|
|
127
|
+
thread_cycle.call
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
#######################
|
|
131
|
+
## Events (Callbacks) / Multi-tasking Platform
|
|
132
|
+
# returns true if there are any unhandled events
|
|
133
|
+
# @api private
|
|
134
|
+
def events?
|
|
135
|
+
self.class.e_locker.synchronize { !self.class.events.empty? }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# pushes an event to the event's stack
|
|
139
|
+
# if a block is passed along, it will be used as a callback: the block will be called with the values returned by the handler's `call` method.
|
|
140
|
+
# @api private
|
|
141
|
+
def push_event(handler, *args, &block)
|
|
142
|
+
if block
|
|
143
|
+
self.class.e_locker.synchronize { self.class.events << [proc { |a| push_event block, handler.call(*a) }, args] }
|
|
144
|
+
else
|
|
145
|
+
self.class.e_locker.synchronize { self.class.events << [handler, args] }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Runs the block asynchronously by pushing it as an event to the event's stack
|
|
150
|
+
#
|
|
151
|
+
# @api private
|
|
152
|
+
def run_async(*args, &block)
|
|
153
|
+
self.class.e_locker.synchronize { self.class.events << [block, args] } if block
|
|
154
|
+
!block.nil?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# creates an asynchronous call to a method, with an optional callback (shortcut)
|
|
158
|
+
# @api private
|
|
159
|
+
def callback(object, method, *args, &block)
|
|
160
|
+
push_event object.method(method), *args, &block
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# event handling FIFO
|
|
164
|
+
# @api private
|
|
165
|
+
def fire_event
|
|
166
|
+
event = self.class.e_locker.synchronize { self.class.events.shift }
|
|
167
|
+
return false unless event
|
|
168
|
+
|
|
169
|
+
begin
|
|
170
|
+
event[0].call(*event[1])
|
|
171
|
+
rescue OpenSSL::SSL::SSLError
|
|
172
|
+
log('SSL Bump - SSL Certificate refused?')
|
|
173
|
+
# rubocop:disable Lint/RescueException
|
|
174
|
+
rescue Exception => e
|
|
175
|
+
raise if e.is_a?(SignalException) || e.is_a?(SystemExit)
|
|
176
|
+
end
|
|
177
|
+
# rubocop:enable Lint/RescueException
|
|
178
|
+
|
|
179
|
+
true
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
#####
|
|
183
|
+
# Reactor
|
|
184
|
+
#
|
|
185
|
+
# IO review code will review the connections and sockets
|
|
186
|
+
# it will accept new connections and react to socket input
|
|
187
|
+
# @api private
|
|
188
|
+
def io_review
|
|
189
|
+
self.class.io_locker.synchronize do
|
|
190
|
+
return false unless self.class.events.empty?
|
|
191
|
+
|
|
192
|
+
united = self.class.services.keys + self.class.io_connection_dic.keys
|
|
193
|
+
return false if united.empty?
|
|
194
|
+
|
|
195
|
+
io_r = IO.select(united, nil, united, 0.1)
|
|
196
|
+
if io_r
|
|
197
|
+
io_r[0].each do |io|
|
|
198
|
+
if self.class.services[io]
|
|
199
|
+
begin
|
|
200
|
+
callback(self, :add_connection, io.accept_nonblock, self.class.services[io])
|
|
201
|
+
rescue Errno::EWOULDBLOCK
|
|
202
|
+
# There's nothing to handle. Swallow the error
|
|
203
|
+
rescue StandardError => e
|
|
204
|
+
log(e.message)
|
|
205
|
+
end
|
|
206
|
+
elsif self.class.io_connection_dic[io]
|
|
207
|
+
callback(self, :get_data, io, self.class.io_connection_dic[io])
|
|
208
|
+
else
|
|
209
|
+
log('what?!')
|
|
210
|
+
remove_connection(io)
|
|
211
|
+
self.class.services.delete(io)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
io_r[2].each do |io|
|
|
215
|
+
(remove_connection(io) || self.class.services.delete(io)).close
|
|
216
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
217
|
+
# Swallow all errors
|
|
218
|
+
true
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
callback self, :clear_connections
|
|
223
|
+
true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
#######################
|
|
227
|
+
# IO - listening sockets (services)
|
|
228
|
+
|
|
229
|
+
# @api private
|
|
230
|
+
def add_service(hostname = 'localhost', port = nil, parameters = {})
|
|
231
|
+
hostname = 'localhost' if hostname.nil? || hostname.empty?
|
|
232
|
+
service = TCPServer.new(hostname, port)
|
|
233
|
+
parameters[:hostname] = hostname
|
|
234
|
+
parameters[:port] = service.local_address.ip_port
|
|
235
|
+
self.class.s_locker.synchronize { self.class.services[service] = parameters }
|
|
236
|
+
callback(self, :log, "Started listening on #{hostname}:#{parameters[:port]}.")
|
|
237
|
+
true
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# @api public
|
|
241
|
+
def stop_services(from_trap = false)
|
|
242
|
+
log('Stopping services')
|
|
243
|
+
if from_trap
|
|
244
|
+
# synchronize is not allowed when called from a trap statement
|
|
245
|
+
stop_all_services
|
|
246
|
+
else
|
|
247
|
+
self.class.s_locker.synchronize do
|
|
248
|
+
stop_all_services
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# @api private
|
|
254
|
+
def stop_all_services
|
|
255
|
+
self.class.services.each do |s, p|
|
|
256
|
+
begin
|
|
257
|
+
s.close
|
|
258
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
259
|
+
# Swallow all errors
|
|
260
|
+
true
|
|
261
|
+
end
|
|
262
|
+
log("Stopped listening on #{p[:hostname]}:#{p[:port]}")
|
|
263
|
+
end
|
|
264
|
+
self.class.services.clear
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# @api public
|
|
268
|
+
def remove_connection_async(io)
|
|
269
|
+
callback(self, :remove_connection, io)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
#####################
|
|
273
|
+
# IO - Active connections handling
|
|
274
|
+
|
|
275
|
+
# @api private
|
|
276
|
+
def stop_connections
|
|
277
|
+
self.class.c_locker.synchronize do
|
|
278
|
+
self.class.io_connection_dic.each_key do |io|
|
|
279
|
+
io.close
|
|
280
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
281
|
+
# Swallow all errors
|
|
282
|
+
true
|
|
283
|
+
end
|
|
284
|
+
self.class.io_connection_dic.clear
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# @api private
|
|
289
|
+
def add_connection(io, service_object)
|
|
290
|
+
conn = ::PuppetEditorServices::Connection::Tcp.new(self, io)
|
|
291
|
+
if io
|
|
292
|
+
self.class.c_locker.synchronize do
|
|
293
|
+
self.class.io_connection_dic[io] = { handler: conn, service: service_object }
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
callback(conn, :post_init)
|
|
297
|
+
rescue Exception => e # rubocop:disable Lint/RescueException Need to swallow all errors here
|
|
298
|
+
callback(self, :log, "Error creating connection #{e.inspect}\n#{e.backtrace}")
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# @api public
|
|
302
|
+
def connection(connection_id)
|
|
303
|
+
self.class.c_locker.synchronize do
|
|
304
|
+
self.class.io_connection_dic.each_value do |v|
|
|
305
|
+
return v[:handler] unless v[:handler].nil? || v[:handler].id != connection_id
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
nil
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# @api private
|
|
312
|
+
def remove_connection(io)
|
|
313
|
+
# This needs to be synchronous
|
|
314
|
+
begin
|
|
315
|
+
self.class.io_connection_dic[io][:handler].unbind
|
|
316
|
+
rescue e
|
|
317
|
+
# Any errors when unbinding the handler should NOT stop the underlying socket
|
|
318
|
+
# from being closed
|
|
319
|
+
log("Error unbinding #{e.inspect}\n#{e.backtrace}")
|
|
320
|
+
end
|
|
321
|
+
connection_count = 0
|
|
322
|
+
self.class.c_locker.synchronize do
|
|
323
|
+
self.class.io_connection_dic.delete io
|
|
324
|
+
connection_count = self.class.io_connection_dic.count
|
|
325
|
+
begin
|
|
326
|
+
io.close
|
|
327
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
328
|
+
# Swallow all errors
|
|
329
|
+
true
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
return unless connection_count.zero? && !@server_options[:stop_on_client_exit].nil? && @server_options[:stop_on_client_exit]
|
|
334
|
+
|
|
335
|
+
callback(self, :log, 'All clients have disconnected. Shutting down server.')
|
|
336
|
+
callback(self, :stop_services)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# clears closed connections from the stack
|
|
340
|
+
# @api private
|
|
341
|
+
def clear_connections
|
|
342
|
+
# Using a SymbolProc here does not work
|
|
343
|
+
# rubocop:disable Style/SymbolProc
|
|
344
|
+
self.class.c_locker.synchronize { self.class.io_connection_dic.delete_if { |c| c.closed? } }
|
|
345
|
+
# rubocop:enable Style/SymbolProc
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'puppet_editor_services/server/base'
|
|
4
|
+
|
|
5
|
+
module PuppetEditorServices
|
|
6
|
+
module Server
|
|
7
|
+
def self.current_server
|
|
8
|
+
@@current_server # This is fine
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.current_server=(value)
|
|
12
|
+
@@current_server = value # rubocop:disable Style/ClassVars This is fine
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|