Skip to content

Commit 55cc029

Browse files
authored
Merge pull request #188 from ace13/GH-187-add_stdio_mode
(GH-#187) Add a stdio mode to the language server
2 parents 5f932a9 + 2de87ea commit 55cc029

File tree

4 files changed

+168
-19
lines changed

4 files changed

+168
-19
lines changed

server/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ Usage: puppet-languageserver.rb [options]
111111
-d, --no-preload Do not preload Puppet information when the language server starts. Default is to preload
112112
--debug=DEBUG Output debug information. Either specify a filename or 'STDOUT'. Default is no debug output
113113
-s, --slow-start Delay starting the TCP Server until Puppet initialisation has completed. Default is to start fast
114+
--stdio Runs the server in stdio mode, without a TCP listener
114115
-h, --help Prints this help
115116
-v, --version Prints the Langauge Server version
116117
```

server/lib/puppet-languageserver.rb

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,32 @@
1-
require 'languageserver/languageserver'
2-
require 'puppet-vscode'
3-
4-
%w[json_rpc_handler message_router server_capabilities document_validator
5-
puppet_parser_helper puppet_helper facter_helper completion_provider hover_provider].each do |lib|
6-
begin
7-
require "puppet-languageserver/#{lib}"
8-
rescue LoadError
9-
require File.expand_path(File.join(File.dirname(__FILE__), 'puppet-languageserver', 'lib'))
1+
begin
2+
original_verbose = $VERBOSE
3+
$VERBOSE = nil
4+
5+
require 'languageserver/languageserver'
6+
require 'puppet-vscode'
7+
8+
%w[json_rpc_handler message_router server_capabilities document_validator
9+
puppet_parser_helper puppet_helper facter_helper completion_provider hover_provider].each do |lib|
10+
begin
11+
require "puppet-languageserver/#{lib}"
12+
rescue LoadError
13+
require File.expand_path(File.join(File.dirname(__FILE__), 'puppet-languageserver', 'lib'))
14+
end
1015
end
11-
end
1216

13-
require 'puppet'
14-
require 'optparse'
15-
require 'logger'
17+
require 'puppet'
18+
require 'optparse'
19+
require 'logger'
20+
ensure
21+
$VERBOSE = original_verbose
22+
end
1623

1724
module PuppetLanguageServer
1825
class CommandLineParser
1926
def self.parse(options)
2027
# Set defaults here
2128
args = {
29+
stdio: false,
2230
port: 8081,
2331
ipaddress: '127.0.0.1',
2432
stop_on_client_exit: true,
@@ -60,6 +68,10 @@ def self.parse(options)
6068
args[:fast_start_tcpserver] = false
6169
end
6270

71+
opts.on('--stdio', "Runs the server in stdio mode, without a TCP listener") do |_misc|
72+
args[:stdio] = true
73+
end
74+
6375
opts.on('--local-workspace=PATH', 'The workspace or file path that will be used to provide module-specific functionality. Default is no workspace path.') do |path|
6476
args[:workspace] = path
6577
end
@@ -122,13 +134,29 @@ def self.init_puppet_worker(options)
122134
def self.rpc_server(options)
123135
log_message(:info, 'Starting RPC Server...')
124136

125-
server = PuppetVSCode::SimpleTCPServer.new
137+
if options[:stdio]
138+
$stdin.sync = true
139+
$stdout.sync = true
140+
141+
handler = PuppetLanguageServer::MessageRouter.new
142+
handler.socket = $stdout
143+
handler.post_init
126144

127-
options[:servicename] = 'LANGUAGE SERVER'
145+
loop do
146+
data = $stdin.readpartial(1048576)
147+
raise 'Receieved an empty input string' if data.length.zero?
128148

129-
server.add_service(options[:ipaddress], options[:port])
130-
trap('INT') { server.stop_services(true) }
131-
server.start(PuppetLanguageServer::MessageRouter, options, 2)
149+
handler.receive_data(data)
150+
end
151+
else
152+
server = PuppetVSCode::SimpleTCPServer.new
153+
154+
options[:servicename] = 'LANGUAGE SERVER'
155+
156+
server.add_service(options[:ipaddress], options[:port])
157+
trap('INT') { server.stop_services(true) }
158+
server.start(PuppetLanguageServer::MessageRouter, options, 2)
159+
end
132160

133161
log_message(:info, 'Language Server exited.')
134162
end

server/lib/puppet-vscode/logging.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def self.log_message(severity, message)
2121
def self.init_logging(options)
2222
if options[:debug].nil?
2323
@logger = nil
24-
elsif options[:debug].casecmp 'stdout'
24+
elsif (options[:debug].casecmp 'stdout').zero?
2525
@logger = Logger.new($stdout)
2626
elsif !options[:debug].to_s.empty?
2727
# Log to file
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
require 'spec_helper'
2+
require 'open3'
3+
require 'socket'
4+
5+
SERVER_TCP_PORT = 8081
6+
SERVER_HOST = '127.0.0.1'
7+
8+
def start_tcp_server(start_options = ['--no-preload','--timeout=5'])
9+
cmd = "ruby puppet-languageserver #{start_options.join(' ')} --port=#{SERVER_TCP_PORT} --ip=0.0.0.0"
10+
11+
stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)
12+
# Wait for the Language Server to indicate it started
13+
line = nil
14+
begin
15+
line = stdout.readline
16+
end until line =~ /LANGUAGE SERVER RUNNING/
17+
stdout.close
18+
stdin.close
19+
stderr.close
20+
wait_thr
21+
end
22+
23+
def start_stdio_server(start_options = ['--no-preload','--timeout=5'])
24+
cmd = "ruby puppet-languageserver #{start_options.join(' ')} --stdio"
25+
26+
stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)
27+
stderr.close
28+
return stdin, stdout, wait_thr
29+
end
30+
31+
def send_message(sender,message)
32+
str = "Content-Length: #{message.length}\r\n\r\n" + message
33+
sender.write(str)
34+
sender.flush
35+
end
36+
37+
def get_response(reader)
38+
sleep(1)
39+
reader.readpartial(2048)
40+
end
41+
42+
describe 'puppet-languageserver' do
43+
describe 'TCP Server' do
44+
before(:each) do
45+
@server_thr = start_tcp_server
46+
@client = TCPSocket.open(SERVER_HOST, SERVER_TCP_PORT)
47+
end
48+
49+
after(:each) do
50+
@client.close unless @client.nil?
51+
52+
begin
53+
Process.kill("KILL", @server_thr[:pid])
54+
Process.wait(@server_thr[:pid])
55+
rescue
56+
# The server process may not exist and checking in a cross platform way in ruby is difficult
57+
# Instead just swallow any errors
58+
end
59+
end
60+
61+
it 'responds to initialize request' do
62+
send_message(@client, '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":1580,"rootPath":"c:\\\\Source\\\\puppet-vscode-files","rootUri":"file:///c%3A/Source/puppet-vscode-files","capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true},"didChangeConfiguration":{"dynamicRegistration":false},"didChangeWatchedFiles":{"dynamicRegistration":false},"symbol":{"dynamicRegistration":true},"executeCommand":{"dynamicRegistration":true}},"textDocument":{"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"completionItem":{"snippetSupport":true}},"hover":{"dynamicRegistration":true},"signatureHelp":{"dynamicRegistration":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"definition":{"dynamicRegistration":true},"codeAction":{"dynamicRegistration":true},"codeLens":{"dynamicRegistration":true},"documentLink":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true}}},"trace":"off"}}')
63+
response = get_response(@client)
64+
65+
expect(response).to match /{"jsonrpc":"2.0","id":0,"result":{"capabilities":/
66+
end
67+
68+
it 'responds to puppet/getVersion request' do
69+
send_message(@client, '{"jsonrpc":"2.0","id":0,"method":"puppet/getVersion"}')
70+
response = get_response(@client)
71+
72+
# Expect the response to have the required parameters
73+
expect(response).to match /"puppetVersion":/
74+
expect(response).to match /"facterVersion":/
75+
expect(response).to match /"functionsLoaded":/
76+
expect(response).to match /"typesLoaded":/
77+
expect(response).to match /"factsLoaded":/
78+
end
79+
end
80+
81+
describe 'STDIO Server' do
82+
before(:each) do
83+
@stdin, @stdout, @server_thr = start_stdio_server
84+
end
85+
86+
after(:each) do
87+
@stdin.close unless @stdin.nil?
88+
@stdout.close unless @stdout.nil?
89+
90+
unless @server_thr.nil?
91+
begin
92+
Process.kill("KILL", @server_thr[:pid])
93+
Process.wait(@server_thr[:pid])
94+
rescue
95+
# The server process may not exist and checking in a cross platform way in ruby is difficult
96+
# Instead just swallow any errors
97+
end
98+
end
99+
end
100+
101+
it 'responds to initialize request' do
102+
send_message(@stdin, '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":1580,"rootPath":"c:\\\\Source\\\\puppet-vscode-files","rootUri":"file:///c%3A/Source/puppet-vscode-files","capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true},"didChangeConfiguration":{"dynamicRegistration":false},"didChangeWatchedFiles":{"dynamicRegistration":false},"symbol":{"dynamicRegistration":true},"executeCommand":{"dynamicRegistration":true}},"textDocument":{"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"completionItem":{"snippetSupport":true}},"hover":{"dynamicRegistration":true},"signatureHelp":{"dynamicRegistration":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"definition":{"dynamicRegistration":true},"codeAction":{"dynamicRegistration":true},"codeLens":{"dynamicRegistration":true},"documentLink":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true}}},"trace":"off"}}')
103+
response = get_response(@stdout)
104+
105+
expect(response).to match /{"jsonrpc":"2.0","id":0,"result":{"capabilities":/
106+
end
107+
108+
it 'responds to puppet/getVersion request' do
109+
send_message(@stdin, '{"jsonrpc":"2.0","id":0,"method":"puppet/getVersion"}')
110+
response = get_response(@stdout)
111+
112+
# Expect the response to have the required parameters
113+
expect(response).to match /"puppetVersion":/
114+
expect(response).to match /"facterVersion":/
115+
expect(response).to match /"functionsLoaded":/
116+
expect(response).to match /"typesLoaded":/
117+
expect(response).to match /"factsLoaded":/
118+
end
119+
end
120+
end

0 commit comments

Comments
 (0)