Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Switch from JSON -> YAML for the command-socket protocol

  • Loading branch information...
commit 665ce7b7c6cb3b7e69028a239bf8d86544e35534 1 parent c279ec2
@gdb gdb authored
View
8 bin/einhorn
@@ -78,6 +78,10 @@ run). Einhorn relies on file permissions to ensure that no malicious
users can gain access. Run with a `-d DIRECTORY` to change the
directory where the socket will live.
+Note that the command socket uses a line-oriented YAML protocol, and
+you should ensure you trust clients to send arbitrary YAML messages
+into your process.
+
### Seamless upgrades
You can cause your code to be seamlessly reloaded by upgrading the
@@ -143,8 +147,8 @@ To use preloading, just give Einhorn a `-p PATH_TO_CODE`, and make
sure you've defined an `einhorn_main` method.
In order to maximize compatibility, we've worked to minimize Einhorn's
-dependencies. At the moment Einhorn imports 'rubygems' and 'json'. (If
-these turn out to be issues, we could probably find a workaround.)
+dependencies. It has no dependencies outside of the Ruby standard
+library.
### Command name
View
3  einhorn.gemspec
@@ -13,8 +13,7 @@ Gem::Specification.new do |gem|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.name = "einhorn"
gem.require_paths = ["lib"]
- # maybe swap out for YAML? Then don't need any gems.
- gem.add_dependency('json')
+
gem.add_development_dependency('shoulda')
gem.add_development_dependency('mocha')
gem.version = Einhorn::VERSION
View
2  lib/einhorn.rb
@@ -6,8 +6,6 @@
require 'tmpdir'
require 'yaml'
-require 'rubygems'
-
module Einhorn
module AbstractState
def default_state; raise NotImplementedError.new('Override in extended modules'); end
View
45 lib/einhorn/client.rb
@@ -1,8 +1,34 @@
-require 'json'
require 'set'
+require 'uri'
+require 'yaml'
module Einhorn
class Client
+ # Keep this in this file so client can be loaded entirely
+ # standalone by user code.
+ module Transport
+ def self.send_message(socket, message)
+ line = serialize_message(message)
+ socket.write(line)
+ end
+
+ def self.receive_message(socket)
+ line = socket.readline
+ deserialize_message(line)
+ end
+
+ def self.serialize_message(message)
+ serialized = YAML.dump(message)
+ escaped = URI.escape(serialized, "%\n")
+ escaped + "\n"
+ end
+
+ def self.deserialize_message(line)
+ serialized = URI.unescape(line)
+ YAML.load(serialized)
+ end
+ end
+
@@responseless_commands = Set.new(['worker:ack'])
def self.for_path(path_to_socket)
@@ -20,9 +46,8 @@ def initialize(socket)
end
def command(command_hash)
- command = JSON.generate(command_hash) + "\n"
- write(command)
- recvmessage if expect_response?(command_hash)
+ Transport.send_message(@socket, command_hash)
+ Transport.receive_message(@socket) if expect_response?(command_hash)
end
def expect_response?(command_hash)
@@ -32,17 +57,5 @@ def expect_response?(command_hash)
def close
@socket.close
end
-
- private
-
- def write(bytes)
- @socket.write(bytes)
- end
-
- # TODO: use a streaming JSON parser instead?
- def recvmessage
- line = @socket.readline
- JSON.parse(line)
- end
end
end
View
1  lib/einhorn/command.rb
@@ -1,7 +1,6 @@
require 'pp'
require 'set'
require 'tmpdir'
-require 'json'
require 'einhorn/command/interface'
View
18 lib/einhorn/command/interface.rb
@@ -201,14 +201,13 @@ def self.send_message(conn, response)
if response.kind_of?(String)
response = {'message' => response}
end
- message = pack_message(response)
- conn.write(message)
+ Einhorn::Client::Transport.send_message(conn, response)
end
def self.generate_response(conn, command)
begin
- request = JSON.parse(command)
- rescue JSON::ParserError => e
+ request = Einhorn::Client::Transport.deserialize_message(command)
+ rescue ArgumentError => e
return {
'message' => "Could not parse command: #{e}"
}
@@ -235,17 +234,6 @@ def self.generate_response(conn, command)
end
end
- def self.pack_message(message_struct)
- begin
- JSON.generate(message_struct) + "\n"
- rescue JSON::GeneratorError => e
- response = {
- 'message' => "Error generating JSON message for #{message_struct.inspect} (this indicates a bug): #{e}"
- }
- JSON.generate(response) + "\n"
- end
- end
-
def self.command_descriptions
command_specs = @@commands.select do |_, spec|
spec[:description]
View
49 test/unit/einhorn/client.rb
@@ -0,0 +1,49 @@
+require File.expand_path(File.join(File.dirname(__FILE__), '../../test_helper'))
+
+require 'einhorn'
+
+class ClientTest < Test::Unit::TestCase
+ def message
+ {:foo => ['%bar', '%baz']}
+ end
+
+ def serialized
+ "--- %0A:foo: %0A- \"%25bar\"%0A- \"%25baz\"%0A\n"
+ end
+
+ context "when sending a message" do
+ should "write a serialized line" do
+ socket = mock
+ socket.expects(:write).with(serialized)
+ Einhorn::Client::Transport.send_message(socket, message)
+ end
+ end
+
+ context "when receiving a message" do
+ should "deserialize a single line" do
+ socket = mock
+ socket.expects(:readline).returns(serialized)
+ result = Einhorn::Client::Transport.receive_message(socket)
+ assert_equal(result, message)
+ end
+ end
+
+ context "when {de,}serializing a message" do
+ should "serialize and escape a message as expected" do
+ actual = Einhorn::Client::Transport.serialize_message(message)
+ assert_equal(serialized, actual)
+ end
+
+ should "deserialize and unescape a message as expected" do
+ actual = Einhorn::Client::Transport.deserialize_message(serialized)
+ assert_equal(message, actual)
+ end
+
+ should "raise an error when deserializing invalid YAML" do
+ invalid_serialized = "-%0A\t-"
+ assert_raises(ArgumentError) do
+ Einhorn::Client::Transport.deserialize_message(invalid_serialized)
+ end
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.