Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(GH-#187) Add a stdio mode to the language server #188

Merged
merged 5 commits into from
Nov 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Usage: puppet-languageserver.rb [options]
-d, --no-preload Do not preload Puppet information when the language server starts. Default is to preload
--debug=DEBUG Output debug information. Either specify a filename or 'STDOUT'. Default is no debug output
-s, --slow-start Delay starting the TCP Server until Puppet initialisation has completed. Default is to start fast
--stdio Runs the server in stdio mode, without a TCP listener
-h, --help Prints this help
-v, --version Prints the Langauge Server version
```
Expand Down
64 changes: 46 additions & 18 deletions server/lib/puppet-languageserver.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
require 'languageserver/languageserver'
require 'puppet-vscode'

%w[json_rpc_handler message_router server_capabilities document_validator
puppet_parser_helper puppet_helper facter_helper completion_provider hover_provider].each do |lib|
begin
require "puppet-languageserver/#{lib}"
rescue LoadError
require File.expand_path(File.join(File.dirname(__FILE__), 'puppet-languageserver', 'lib'))
begin
original_verbose = $VERBOSE
$VERBOSE = nil

require 'languageserver/languageserver'
require 'puppet-vscode'

%w[json_rpc_handler message_router server_capabilities document_validator
puppet_parser_helper puppet_helper facter_helper completion_provider hover_provider].each do |lib|
begin
require "puppet-languageserver/#{lib}"
rescue LoadError
require File.expand_path(File.join(File.dirname(__FILE__), 'puppet-languageserver', 'lib'))
end
end
end

require 'puppet'
require 'optparse'
require 'logger'
require 'puppet'
require 'optparse'
require 'logger'
ensure
$VERBOSE = original_verbose
end

module PuppetLanguageServer
class CommandLineParser
def self.parse(options)
# Set defaults here
args = {
stdio: false,
port: 8081,
ipaddress: '127.0.0.1',
stop_on_client_exit: true,
Expand Down Expand Up @@ -60,6 +68,10 @@ def self.parse(options)
args[:fast_start_tcpserver] = false
end

opts.on('--stdio', "Runs the server in stdio mode, without a TCP listener") do |_misc|
args[:stdio] = true
end

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|
args[:workspace] = path
end
Expand Down Expand Up @@ -122,13 +134,29 @@ def self.init_puppet_worker(options)
def self.rpc_server(options)
log_message(:info, 'Starting RPC Server...')

server = PuppetVSCode::SimpleTCPServer.new
if options[:stdio]
$stdin.sync = true
$stdout.sync = true

handler = PuppetLanguageServer::MessageRouter.new
handler.socket = $stdout
handler.post_init

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

server.add_service(options[:ipaddress], options[:port])
trap('INT') { server.stop_services(true) }
server.start(PuppetLanguageServer::MessageRouter, options, 2)
handler.receive_data(data)
end
else
server = PuppetVSCode::SimpleTCPServer.new

options[:servicename] = 'LANGUAGE SERVER'

server.add_service(options[:ipaddress], options[:port])
trap('INT') { server.stop_services(true) }
server.start(PuppetLanguageServer::MessageRouter, options, 2)
end

log_message(:info, 'Language Server exited.')
end
Expand Down
2 changes: 1 addition & 1 deletion server/lib/puppet-vscode/logging.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def self.log_message(severity, message)
def self.init_logging(options)
if options[:debug].nil?
@logger = nil
elsif options[:debug].casecmp 'stdout'
elsif (options[:debug].casecmp 'stdout').zero?
@logger = Logger.new($stdout)
elsif !options[:debug].to_s.empty?
# Log to file
Expand Down
120 changes: 120 additions & 0 deletions server/spec/integration/puppet-languageserver_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
require 'spec_helper'
require 'open3'
require 'socket'

SERVER_TCP_PORT = 8081
SERVER_HOST = '127.0.0.1'

def start_tcp_server(start_options = ['--no-preload','--timeout=5'])
cmd = "ruby puppet-languageserver #{start_options.join(' ')} --port=#{SERVER_TCP_PORT} --ip=0.0.0.0"

stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)
# Wait for the Language Server to indicate it started
line = nil
begin
line = stdout.readline
end until line =~ /LANGUAGE SERVER RUNNING/
stdout.close
stdin.close
stderr.close
wait_thr
end

def start_stdio_server(start_options = ['--no-preload','--timeout=5'])
cmd = "ruby puppet-languageserver #{start_options.join(' ')} --stdio"

stdin, stdout, stderr, wait_thr = Open3.popen3(cmd)
stderr.close
return stdin, stdout, wait_thr
end

def send_message(sender,message)
str = "Content-Length: #{message.length}\r\n\r\n" + message
sender.write(str)
sender.flush
end

def get_response(reader)
sleep(1)
reader.readpartial(2048)
end

describe 'puppet-languageserver' do
describe 'TCP Server' do
before(:each) do
@server_thr = start_tcp_server
@client = TCPSocket.open(SERVER_HOST, SERVER_TCP_PORT)
end

after(:each) do
@client.close unless @client.nil?

begin
Process.kill("KILL", @server_thr[:pid])
Process.wait(@server_thr[:pid])
rescue
# The server process may not exist and checking in a cross platform way in ruby is difficult
# Instead just swallow any errors
end
end

it 'responds to initialize request' do
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"}}')
response = get_response(@client)

expect(response).to match /{"jsonrpc":"2.0","id":0,"result":{"capabilities":/
end

it 'responds to puppet/getVersion request' do
send_message(@client, '{"jsonrpc":"2.0","id":0,"method":"puppet/getVersion"}')
response = get_response(@client)

# Expect the response to have the required parameters
expect(response).to match /"puppetVersion":/
expect(response).to match /"facterVersion":/
expect(response).to match /"functionsLoaded":/
expect(response).to match /"typesLoaded":/
expect(response).to match /"factsLoaded":/
end
end

describe 'STDIO Server' do
before(:each) do
@stdin, @stdout, @server_thr = start_stdio_server
end

after(:each) do
@stdin.close unless @stdin.nil?
@stdout.close unless @stdout.nil?

unless @server_thr.nil?
begin
Process.kill("KILL", @server_thr[:pid])
Process.wait(@server_thr[:pid])
rescue
# The server process may not exist and checking in a cross platform way in ruby is difficult
# Instead just swallow any errors
end
end
end

it 'responds to initialize request' do
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"}}')
response = get_response(@stdout)

expect(response).to match /{"jsonrpc":"2.0","id":0,"result":{"capabilities":/
end

it 'responds to puppet/getVersion request' do
send_message(@stdin, '{"jsonrpc":"2.0","id":0,"method":"puppet/getVersion"}')
response = get_response(@stdout)

# Expect the response to have the required parameters
expect(response).to match /"puppetVersion":/
expect(response).to match /"facterVersion":/
expect(response).to match /"functionsLoaded":/
expect(response).to match /"typesLoaded":/
expect(response).to match /"factsLoaded":/
end
end
end