waithook 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 119debffa9104d9f4b3b0c1d2a943840a0bb1aaa
4
- data.tar.gz: ee89ce35a2bc6a13a3517f9c524b108d53c36d16
3
+ metadata.gz: 2d3624c5bebd573d8f2e33f66d96d19b93acc3a9
4
+ data.tar.gz: 25d37ca7b8c923b24c04425ce35f75ed997d9033
5
5
  SHA512:
6
- metadata.gz: e75927f077b2fa65d3a3af0855f314f68a4039996ae11ab641b1b2bdc58b43aaefa35196d2f6bf5728edde917729de5ed67880e58abcd85f3c57f6065a656e28
7
- data.tar.gz: 07000675d72ef66ab760dde7d039e4fcbafa36186d4f06ecc18e0912ad4b829d89dcfb2454c110480f190dce8017cade2acba2adddac6548c69c48768e9f29c0
6
+ metadata.gz: 32a2181e21aa618a589c64d7dbd07132ebdc1942c4169bfe26019b8061d05ea18a494d0ffb8e2da343accba7307427c9c69742ed3a05477897ff3d91dd3de847
7
+ data.tar.gz: 3a96f332074cbd7a5e8364bcebbc42de580ca8575f39ad17c19c28d0f012320c09cd36e7df2d18ad664bd43a3852a682ad79bb608fd3ca7796fb4cf090f930b3
data/Gemfile.lock CHANGED
@@ -2,7 +2,7 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  waithook (0.1)
5
- websocket (~> 1.2.3)
5
+ websocket (~> 1.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
data/bin/waithook CHANGED
@@ -2,3 +2,49 @@
2
2
 
3
3
  $:.push(File.expand_path("../../lib", __FILE__))
4
4
 
5
+ require 'optparse'
6
+
7
+ def help_message
8
+ [
9
+ "Waithook command line client.",
10
+ "",
11
+ "Example usage:",
12
+ " waithook waithook.herokuapp.com/my_path",
13
+ " waithook waithook.herokuapp.com/my_path --forward http://localhost:3000/notify"
14
+ ].join("\n")
15
+ end
16
+
17
+ options = {}
18
+
19
+ begin
20
+ OptionParser.new do |opts|
21
+ opts.banner = help_message + "\nOptions:"
22
+
23
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
24
+ options[:verbose] = v
25
+ end
26
+ opts.on("-f URL", "--forward URL", "Forward to HTTP server") do |value|
27
+ options[:forward] = value
28
+ end
29
+ end.parse!
30
+ rescue OptionParser::InvalidOption => error
31
+ require 'waithook'
32
+ puts Waithook::WithColor.red(error.message)
33
+ puts "Please use --help to list of arguments"
34
+ exit 1
35
+ end
36
+
37
+ begin
38
+ require 'waithook'
39
+ if ARGV.first
40
+ Waithook::CLI.listen(ARGV.first, options)
41
+ else
42
+ puts Waithook::WithColor.red("URL is required\n")
43
+ puts help_message
44
+ exit 1
45
+ end
46
+ rescue Waithook::CLI::ArgError => error
47
+ puts Waithook::WithColor.red(error.message)
48
+ puts help_message
49
+ exit 1
50
+ end
@@ -0,0 +1,64 @@
1
+ class Waithook
2
+ module WithColor
3
+ extend self
4
+ def black(s); "\033[30m#{s}\033[0m" end
5
+ def red(s); "\033[31m#{s}\033[0m" end
6
+ def green(s); "\033[32m#{s}\033[0m" end
7
+ def brown(s); "\033[33m#{s}\033[0m" end
8
+ def blue(s); "\033[34m#{s}\033[0m" end
9
+ def magenta(s); "\033[35m#{s}\033[0m" end
10
+ def cyan(s); "\033[36m#{s}\033[0m" end
11
+ def gray(s); "\033[37m#{s}\033[0m" end
12
+ def yellow(s); "\033[93m#{s}\033[0m" end
13
+ def bold(s); "\e[1m#{s}\e[m" end
14
+ end
15
+
16
+ module CLI
17
+ class ArgError < ArgumentError; end
18
+
19
+ extend self
20
+
21
+ def listen(url, options)
22
+ puts "Run with options: #{options}" if options[:verbose]
23
+ unless url.start_with?('ws://', 'wss://')
24
+ url = 'wss://' + url
25
+ end
26
+
27
+ unless url =~ /\A#{URI::regexp(['ws', 'wss'])}\z/
28
+ raise ArgError, "#{url.inspect} is not a valid websocket URL"
29
+ end
30
+
31
+ uri = URI.parse(url)
32
+ port = uri.scheme == 'wss' ? 443 : uri.port
33
+ path = uri.path.start_with?('/') ? uri.path.sub(/^\//, '') : uri.path
34
+ logger_level = options[:verbose] ? 'trace' : 'warn'
35
+
36
+ puts WithColor.green("Connecting to #{url}")
37
+ waithook = Waithook.new(host: uri.host, port: port, path: path, logger_level: logger_level)
38
+ puts WithColor.green("Connected! Waiting to for message...") if waithook.client.wait_connected
39
+
40
+ while true
41
+ message = waithook.wait_message
42
+ puts message.message
43
+ if options[:forward]
44
+ Thread.new do
45
+ begin
46
+ forward_url = if options[:forward].start_with?('http://', 'https://')
47
+ forward_url
48
+ else
49
+ "http://#{options[:forward]}"
50
+ end
51
+
52
+ puts WithColor.brown("Sending as HTTP to #{forward_url}")
53
+ response = message.send_to(forward_url)
54
+ puts WithColor.brown("Reponse from #{forward_url} -> #{WithColor.bold("#{response.code} #{response.message}")}")
55
+ rescue => error
56
+ puts WithColor.red("#{error.message} (#{error.class})")
57
+ puts WithColor.red(error.backtrace.join("\n"))
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ require 'logger'
2
+
3
+ # Just add trace level
4
+ class LoggerWithTrace < ::Logger
5
+ module Severity
6
+ include ::Logger::Severity
7
+ TRACE = -1
8
+ end
9
+
10
+ TRACE = Severity::TRACE
11
+
12
+ def trace(progname = nil, &block)
13
+ add(TRACE, nil, progname, &block)
14
+ end
15
+
16
+ def trace?
17
+ @level <= TRACE
18
+ end
19
+
20
+ def level=(severity)
21
+ if severity.is_a?(Integer)
22
+ @level = severity
23
+ else
24
+ @level = case severity.to_s.downcase
25
+ when 'trace'.freeze then TRACE
26
+ when 'debug'.freeze then DEBUG
27
+ when 'info'.freeze then INFO
28
+ when 'warn'.freeze then WARN
29
+ when 'error'.freeze then ERROR
30
+ when 'fatal'.freeze then FATAL
31
+ when 'unknown'.freeze then UNKNOWN
32
+ else
33
+ raise ArgumentError, "invalid log level: #{severity}"
34
+ end
35
+ end
36
+ end
37
+
38
+ def setup(options)
39
+ self.progname = options[:progname]
40
+ self.formatter = proc do |serverity, time, progname, msg|
41
+ msg.lines.map do |line|
42
+ "#{progname} :: #{line}"
43
+ end.join("") + "\n"
44
+ end
45
+ self.level = options[:level]
46
+
47
+ self
48
+ end
49
+ end
@@ -1,3 +1,3 @@
1
1
  class Waithook
2
- VERSION = "0.1"
2
+ VERSION = "0.2"
3
3
  end
@@ -1,6 +1,8 @@
1
1
  require 'socket'
2
- require 'logger'
3
2
  require 'websocket'
3
+ require 'stringio'
4
+
5
+ require_relative 'logger_with_trace'
4
6
 
5
7
  class Waithook
6
8
  class WebsocketClient
@@ -29,26 +31,24 @@ class Waithook
29
31
  @options = options
30
32
 
31
33
  @waiters = []
34
+ @connect_waiters = []
32
35
  @handshake_received = false
33
36
  @messages = Queue.new
34
37
  @is_open = false
35
38
 
36
- @output = options[:output] || STDOUT
39
+ @output = options[:output] || $stdout
37
40
 
38
41
  if options[:logger] === false
39
42
  @output = StringIO.new
40
43
  end
41
44
 
42
- if options[:logger]
45
+ if options[:logger] && options[:logger] != true
43
46
  @logger = options[:logger]
44
47
  else
45
- @logger = Logger.new(@output)
46
- @logger.progname = self.class.name
47
- @logger.formatter = proc do |serverity, time, progname, msg|
48
- msg.lines.map do |line|
49
- "#{progname} :: #{line}"
50
- end.join("") + "\n"
51
- end
48
+ @logger = LoggerWithTrace.new(@output).setup(
49
+ progname: self.class.name,
50
+ level: options[:logger_level] || :info
51
+ )
52
52
  end
53
53
  end
54
54
 
@@ -74,7 +74,7 @@ class Waithook
74
74
  @is_open = true
75
75
  @handshake = WebSocket::Handshake::Client.new(url: "ws://#{@host}/#{@path}")
76
76
 
77
- logger.debug "Sending handshake:\n#{@handshake}"
77
+ logger.trace "Sending handshake:\n#{@handshake}"
78
78
 
79
79
  @socket.print(@handshake)
80
80
  _start_parser!
@@ -94,10 +94,10 @@ class Waithook
94
94
  logger.debug "Start reading in thread"
95
95
  handshake_response = _wait_handshake_response
96
96
  @handshake << handshake_response
97
- logger.debug "Handshake received:\n #{handshake_response}"
97
+ logger.trace "Handshake received:\n #{handshake_response}"
98
98
 
99
99
  @frame_parser = WebSocket::Frame::Incoming::Client.new
100
- @handshake_received = true
100
+ _handshake_recieved!
101
101
  _wait_frames!
102
102
  rescue Object => error
103
103
  logger.error "#{error.class}: #{error.message}\n#{error.backtrace.join("\n")}"
@@ -135,6 +135,20 @@ class Waithook
135
135
  @messages.pop
136
136
  end
137
137
 
138
+ def wait_connected
139
+ return true if @handshake_received
140
+ waiter = Waiter.new
141
+ @connect_waiters << waiter
142
+ waiter.wait
143
+ end
144
+
145
+ def _handshake_recieved!
146
+ @handshake_received = true
147
+ while waiter = @connect_waiters.shift
148
+ waiter.notify(true)
149
+ end
150
+ end
151
+
138
152
  def _notify_waiters(type, payload)
139
153
  while waiter = @waiters.shift
140
154
  waiter.notify([type, payload])
@@ -144,11 +158,13 @@ class Waithook
144
158
  def _send_frame(type, payload = nil)
145
159
  wait_handshake!
146
160
  frame = WebSocket::Frame::Outgoing::Client.new(version: @handshake.version, data: payload, type: type)
147
- logger.debug "Sending :#{frame.type} #{payload ? "DATA: #{frame.data}" : "(no data)"}"
161
+ logger.trace "Sending :#{frame.type} #{payload ? "DATA: #{frame.data}" : "(no data)"}"
148
162
  @socket.write(frame.to_s)
149
163
  end
150
164
 
151
165
  def _process_frame(message)
166
+ logger.trace "Received :#{message.type} #{message.data ? "DATA: #{message.data}" : "(no data)"}"
167
+
152
168
  if message.type == :ping
153
169
  send_pong!
154
170
  end
data/lib/waithook.rb CHANGED
@@ -1,15 +1,20 @@
1
1
  require 'net/http'
2
+ require 'uri'
3
+ require 'timeout'
2
4
  require 'json'
5
+ require 'stringio'
3
6
 
7
+ require_relative 'waithook/logger_with_trace'
4
8
  require_relative 'waithook/websocket_client'
9
+ require_relative 'waithook/cli'
5
10
 
6
11
  class Waithook
7
12
 
8
13
  SERVER_HOST = "waithook.herokuapp.com"
9
14
  SERVER_PORT = 443
10
15
 
11
- def self.subscribe(path, options = {}, &block)
12
- instance = new(path, options)
16
+ def self.subscribe(options = {}, &block)
17
+ instance = new(options)
13
18
  if block
14
19
  instance.filter = block
15
20
  end
@@ -17,30 +22,52 @@ class Waithook
17
22
  instance
18
23
  end
19
24
 
25
+ def self.default_path
26
+ @default_path
27
+ end
28
+
29
+ def self.default_path=(value)
30
+ @default_path = value
31
+ end
32
+
20
33
  attr_accessor :filter
21
34
  attr_accessor :messages
22
35
  attr_reader :client
36
+ attr_reader :options
23
37
 
24
- def initialize(path, options = {})
25
- options = {
38
+ def initialize(options = {})
39
+ @options = {
26
40
  host: SERVER_HOST,
27
41
  port: SERVER_PORT,
28
- auto_connect: true
42
+ auto_connect: true,
43
+ path: self.class.default_path
29
44
  }.merge(options)
30
45
 
31
- @path = path
46
+ if @options[:path] == nil
47
+ raise ArgumentError, ":path is missing. Please add :path to options argument or set Waithook.default_path = 'foo'"
48
+ end
49
+
32
50
  @client = WebsocketClient.new(
33
- path: path,
34
- host: options[:host],
35
- port: options[:port],
36
- logger: options[:logger]
51
+ path: @options[:path],
52
+ host: @options[:host],
53
+ port: @options[:port],
54
+ logger: @options[:logger],
55
+ logger_level: @options[:logger_level],
56
+ output: @options[:output]
37
57
  )
38
58
 
39
59
  @messages = []
40
60
  @filter = nil
41
61
  @started = false
42
62
 
43
- connect! if options[:auto_connect]
63
+ connect! if @options[:auto_connect]
64
+ end
65
+
66
+ def logger
67
+ @logger ||= LoggerWithTrace.new(@options[:logger] ? $stdout : StringIO.new).setup(
68
+ progname: self.class.name,
69
+ level: @options[:logger_level] || :info
70
+ )
44
71
  end
45
72
 
46
73
  def connect!
@@ -54,50 +81,31 @@ class Waithook
54
81
  !!@started
55
82
  end
56
83
 
57
- def forward_to(url)
58
- webhook = wait_message
59
-
60
- uri = URI.parse(url)
61
- response = nil
62
-
63
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
64
- http_klass = case webhook.method
65
- when "GET" then Net::HTTP::Get
66
- when "POST" then Net::HTTP::Post
67
- when "PUT" then Net::HTTP::Put
68
- when "PATCH" then Net::HTTP::Patch
69
- when "HEAD" then Net::HTTP::Head
70
- when "DELETE" then Net::HTTP::Delete
71
- when "MOVE" then Net::HTTP::Move
72
- when "COPY" then Net::HTTP::Copy
73
- when "HEAD" then Net::HTTP::Head
74
- else Net::HTTP::Post
75
- end
76
-
77
- request = http_klass.new(uri)
78
- webhook.headers.each do |key, value|
79
- request[key] = value
80
- end
81
-
82
- if webhook.body
83
- request.body = webhook.body
84
- end
85
-
86
- response = http.request(request)
87
- end
88
-
89
- response
84
+ def forward_to(url, options = {})
85
+ webhook = wait_message(options)
86
+ webhook.send_to(url) unless webhook.nil?
90
87
  end
91
88
 
92
- def wait_message
93
- while true
94
- type, data = @client.wait_message
95
- webhook = Webhook.new(data)
96
- if @filter && @filter.call(webhook) || !@filter
97
- @messages << webhook
98
- return webhook
89
+ def wait_message(options = {})
90
+ raise_timeout_error = options.has_key?(:raise_on_timeout) ? options[:raise_on_timeout] : true
91
+ timeout = options[:timeout] || @options[:timeout] || 0
92
+
93
+ start_time = Time.new.to_f
94
+ Timeout.timeout(timeout) do
95
+ while true
96
+ type, data = @client.wait_message
97
+ webhook = Webhook.new(data)
98
+ if @filter && @filter.call(webhook) || !@filter
99
+ @messages << webhook
100
+ return webhook
101
+ end
99
102
  end
100
103
  end
104
+ rescue Timeout::Error => error
105
+ time_diff = (Time.now.to_f - start_time).round(3)
106
+ logger.error "#{error.class}: #{error.message} (after #{time_diff} seconds)"
107
+ raise error if raise_timeout_error
108
+ return nil
101
109
  end
102
110
 
103
111
  def close!
@@ -110,9 +118,11 @@ class Waithook
110
118
  attr_reader :headers
111
119
  attr_reader :body
112
120
  attr_reader :method
121
+ attr_reader :message
113
122
 
114
123
  def initialize(payload)
115
- data = JSON.parse(payload)
124
+ @message = payload
125
+ data = JSON.parse(@message)
116
126
  @url = data['url']
117
127
  @headers = data['headers']
118
128
  @body = data['body']
@@ -124,5 +134,38 @@ class Waithook
124
134
  @json_body ||= JSON.parse(@body)
125
135
  end
126
136
  end
137
+
138
+ def send_to(url)
139
+ uri = URI.parse(url)
140
+ response = nil
141
+
142
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
143
+ http_klass = case method
144
+ when "GET" then Net::HTTP::Get
145
+ when "POST" then Net::HTTP::Post
146
+ when "PUT" then Net::HTTP::Put
147
+ when "PATCH" then Net::HTTP::Patch
148
+ when "HEAD" then Net::HTTP::Head
149
+ when "DELETE" then Net::HTTP::Delete
150
+ when "MOVE" then Net::HTTP::Move
151
+ when "COPY" then Net::HTTP::Copy
152
+ when "HEAD" then Net::HTTP::Head
153
+ else Net::HTTP::Post
154
+ end
155
+
156
+ request = http_klass.new(uri)
157
+ headers.each do |key, value|
158
+ request[key] = value
159
+ end
160
+
161
+ if body
162
+ request.body = body
163
+ end
164
+
165
+ response = http.request(request)
166
+ end
167
+
168
+ response
169
+ end
127
170
  end
128
171
  end
@@ -11,7 +11,7 @@ describe "Waithook" do
11
11
  end
12
12
 
13
13
  def default_client(options = {})
14
- client = Waithook.subscribe(options[:path] || 'my-super-test', host: HOST, port: PORT, logger: false)
14
+ client = Waithook.subscribe({path: 'my-super-test', host: HOST, port: PORT, logger: false}.merge(options))
15
15
  @waithook_instances.push(client)
16
16
  client
17
17
  end
@@ -54,4 +54,46 @@ describe "Waithook" do
54
54
  assert_equal(message, webhook.json_body)
55
55
  assert_equal("POST", webhook.method)
56
56
  end
57
+
58
+ it "should have trace log level" do
59
+ out, err = capture_io do
60
+ default_client(logger_level: :trace, logger: true)
61
+ end
62
+
63
+ assert_includes(out, 'Sec-WebSocket-Version')
64
+ end
65
+
66
+ it "should be more quiet generally" do
67
+ out, err = capture_io do
68
+ default_client(logger: true)
69
+ end
70
+
71
+ refute_includes(out, 'Sec-WebSocket-Version')
72
+ end
73
+
74
+ it "wait_message should raise exception after timeout" do
75
+ assert_raises(Timeout::Error) do
76
+ default_client.wait_message(timeout: 0.1)
77
+ end
78
+ end
79
+
80
+ it "wait_message should return nil after timeout" do
81
+ waithook = default_client
82
+ assert_equal(nil, waithook.wait_message(timeout: 0.1, raise_on_timeout: false))
83
+ end
84
+
85
+ it "forward_to should raise exception after timeout" do
86
+ assert_raises(Timeout::Error) do
87
+ default_client.forward_to('', timeout: 0.1)
88
+ end
89
+ end
90
+
91
+ it "forward_to should return nil after timeout" do
92
+ out, err = capture_io do
93
+ waithook = default_client(logger: true)
94
+ assert_equal(nil, waithook.forward_to('', timeout: 0.1, raise_on_timeout: false))
95
+ end
96
+
97
+ assert_includes(out, "Timeout::Error: execution expired")
98
+ end
57
99
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waithook
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Evstigneev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-27 00:00:00.000000000 Z
11
+ date: 2016-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: websocket
@@ -42,6 +42,8 @@ files:
42
42
  - example/real_server_test.rb
43
43
  - example/waithook_example.rb
44
44
  - lib/waithook.rb
45
+ - lib/waithook/cli.rb
46
+ - lib/waithook/logger_with_trace.rb
45
47
  - lib/waithook/version.rb
46
48
  - lib/waithook/websocket_client.rb
47
49
  - tests/server_test.rb