midori.rb 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +9 -0
  3. data/LICENSE +21 -0
  4. data/ext/midori/extconf.rb +4 -0
  5. data/ext/midori/websocket.c +32 -0
  6. data/lib/midori/api.rb +426 -0
  7. data/lib/midori/api_engine.rb +109 -0
  8. data/lib/midori/clean_room.rb +24 -0
  9. data/lib/midori/configure.rb +12 -0
  10. data/lib/midori/connection.rb +59 -0
  11. data/lib/midori/const.rb +118 -0
  12. data/lib/midori/core_ext/configurable.rb +33 -0
  13. data/lib/midori/core_ext/define_class.rb +29 -0
  14. data/lib/midori/core_ext/proc.rb +13 -0
  15. data/lib/midori/core_ext/string.rb +29 -0
  16. data/lib/midori/env.rb +18 -0
  17. data/lib/midori/eventsource.rb +20 -0
  18. data/lib/midori/exception.rb +22 -0
  19. data/lib/midori/logger.rb +15 -0
  20. data/lib/midori/middleware.rb +31 -0
  21. data/lib/midori/request.rb +115 -0
  22. data/lib/midori/response.rb +34 -0
  23. data/lib/midori/route.rb +20 -0
  24. data/lib/midori/runner.rb +63 -0
  25. data/lib/midori/sandbox.rb +46 -0
  26. data/lib/midori/server.rb +106 -0
  27. data/lib/midori/version.rb +5 -0
  28. data/lib/midori/websocket.rb +105 -0
  29. data/lib/midori.rb +37 -0
  30. data/midori.sublime-project +16 -0
  31. data/tutorial/README.md +11 -0
  32. data/tutorial/SUMMARY.md +28 -0
  33. data/tutorial/advanced/custom_extensions.md +0 -0
  34. data/tutorial/advanced/deploying_for_production.md +0 -0
  35. data/tutorial/advanced/error_handling.md +0 -0
  36. data/tutorial/advanced/rerl.md +0 -0
  37. data/tutorial/advanced/scaling_project.md +0 -0
  38. data/tutorial/advanced/unit_testing.md +0 -0
  39. data/tutorial/essentials/extensions.md +31 -0
  40. data/tutorial/essentials/getting_started.md +27 -0
  41. data/tutorial/essentials/installation.md +93 -0
  42. data/tutorial/essentials/middlewares.md +88 -0
  43. data/tutorial/essentials/request_handling.md +74 -0
  44. data/tutorial/essentials/routing.md +136 -0
  45. data/tutorial/essentials/runner.md +59 -0
  46. data/tutorial/meta/comparison_with_other_frameworks.md +0 -0
  47. data/tutorial/meta/join_the_midori_community.md +0 -0
  48. data/tutorial/meta/next_steps.md +0 -0
  49. metadata +155 -0
@@ -0,0 +1,59 @@
1
+ ##
2
+ # States of a connection
3
+ class Midori::Connection
4
+ include Midori::Server
5
+
6
+ # @!attribute data
7
+ # @return [String] string buffer of data to send
8
+ attr_accessor :data
9
+
10
+ # Init Connection with socket
11
+ # @param [IO] socket raw socket
12
+ def initialize(socket)
13
+ @registered = false
14
+ @socket = socket
15
+ @monitor = nil
16
+ @close_flag = false
17
+ @data = ''
18
+ listen(socket)
19
+ end
20
+
21
+ # Register events of connection
22
+ # @param [IO] socket raw socket
23
+ def listen(socket)
24
+ EventLoop.register(socket, :rw) do |monitor|
25
+ @monitor = monitor
26
+ if monitor.readable?
27
+ receive_data(monitor)
28
+ end
29
+ if monitor.writable?
30
+ if !@data == ''
31
+ # :nocov:
32
+ # Leave for corner cases
33
+ monitor.io.write_nonblock(@data)
34
+ @data = ''
35
+ # :nocov:
36
+ elsif @close_flag
37
+ close_connection
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # Send message to client
44
+ # @param [String] data data to send
45
+ def send_data(data)
46
+ @monitor.writable? ? @socket.write_nonblock(data) : @data << data
47
+ end
48
+
49
+ # Close the connection
50
+ def close_connection
51
+ EventLoop.deregister @socket
52
+ @socket.close
53
+ end
54
+
55
+ # Close the connection after writing
56
+ def close_connection_after_writing
57
+ @close_flag = true
58
+ end
59
+ end
@@ -0,0 +1,118 @@
1
+ ##
2
+ # Module for store Midori Const
3
+ module Midori::Const
4
+ # Hash table for converting numbers to HTTP/1.1 status code line
5
+ STATUS_CODE = {
6
+ 100 => '100 Continue',
7
+ 101 => '101 Switching Protocols',
8
+ 102 => '102 Processing', # RFC 2518
9
+ 200 => '200 OK',
10
+ 201 => '201 Created',
11
+ 202 => '202 Accepted',
12
+ 203 => '203 Non-Authoritative Information',
13
+ 204 => '204 No Content',
14
+ 205 => '205 Reset Content',
15
+ 206 => '206 Partial Content', # RFC 7233
16
+ 207 => '207 Multi-Status', # RFC 4918
17
+ 208 => '208 Already Reported', # RFC 5842
18
+ 226 => '226 IM Used', # RFC 3229
19
+ 300 => '300 Multiple Choices',
20
+ 301 => '301 Moved Permanently',
21
+ 302 => '302 Found',
22
+ 303 => '303 See Other',
23
+ 304 => '304 Not Modified', # RFC 7232
24
+ 305 => '305 Use Proxy',
25
+ 307 => '307 Temporary Redirect',
26
+ 308 => '308 Permanent Redirect', # RFC 7538
27
+ 400 => '400 Bad Request',
28
+ 401 => '401 Unauthorized', # RFC 7235
29
+ 402 => '402 Payment Required',
30
+ 403 => '403 Forbidden',
31
+ 404 => '404 Not Found',
32
+ 405 => '405 Method Not Allowed',
33
+ 406 => '406 Not Acceptable',
34
+ 407 => '407 Proxy Authentication Required', # RFC 7235
35
+ 408 => '408 Request Time-out',
36
+ 409 => '409 Conflict',
37
+ 410 => '410 Gone',
38
+ 411 => '411 Length Required',
39
+ 412 => '412 Precondition Failed', # RFC 7232
40
+ 413 => '413 Payload Too Large', # RFC 7231
41
+ 414 => '414 URI Too Long', # RFC 7231
42
+ 415 => '415 Unsupported Media Type',
43
+ 416 => '416 Range Not Satisfiable',
44
+ 417 => '417 Expectation Failed', # RFC 2324
45
+ 418 => '418 I\'m a teaport', # RFC 2324
46
+ 421 => '421 Misdirected Request', # RFC 7540
47
+ 422 => '422 Unprocessable Entity', # RFC 4918
48
+ 423 => '423 Locked', # RFC 4918
49
+ 424 => '424 Failed Dependency', # RFC 4918
50
+ 426 => '426 Upgrade Required',
51
+ 428 => '428 Precondition Required', # RFC 6585
52
+ 429 => '429 Too Many Requests', # RFC 6585
53
+ 431 => '431 Request Header Fields Too Large', # RFC 6585
54
+ 451 => '451 Unavailable For Legal Reasons',
55
+ 500 => '500 Internal Server Error',
56
+ 501 => '501 Not Implemented',
57
+ 502 => '502 Bad Gateway',
58
+ 503 => '503 Service Unavailable',
59
+ 504 => '504 Gateway Time-out',
60
+ 505 => '505 HTTP Version not supported',
61
+ 506 => '506 Variant Also Negotiates', # RFC 2295
62
+ 507 => '507 Insufficient Storage', # RFC 4918
63
+ 508 => '508 Loop Detected', # RFC 5842
64
+ 510 => '510 Not Extended', # RFC 2774
65
+ 511 => '511 Network Authentication Required', # RFC 6585
66
+ }
67
+ STATUS_CODE.default = '500 Internal Server Error'
68
+ STATUS_CODE.freeze
69
+
70
+ # Default header for Basic HTTP response
71
+ DEFAULT_HEADER = {
72
+ 'Server' => "Midori/#{Midori::VERSION}"
73
+ }
74
+
75
+ # Default header for EventSource response
76
+ EVENTSOURCE_HEADER = {
77
+ 'Content-Type' => 'text-event-stream',
78
+ 'Cache-Control' => 'no-cache',
79
+ 'Connection' => 'keep-alive'
80
+ }
81
+
82
+ # Default header for WebSocket response
83
+ WEBSOCKET_HEADER = {
84
+ 'Upgrade' => 'websocket',
85
+ 'Connection' => 'Upgrade'
86
+ }
87
+
88
+ # Array of Route Methods
89
+ ROUTE_METHODS = %i(
90
+ DELETE
91
+ GET
92
+ HEAD
93
+ POST
94
+ PUT
95
+ CONNECT
96
+ OPTIONS
97
+ TRACE
98
+ COPY
99
+ LOCK
100
+ MKCOL
101
+ MOVE
102
+ PROPFIND
103
+ PROPPATCH
104
+ UNLOCK
105
+ REPORT
106
+ MKACTIVITY
107
+ CHECKOUT
108
+ MERGE
109
+ M-SEARCH
110
+ NOTIFY
111
+ SUBSCRIBE
112
+ UNSUBSCRIBE
113
+ PATCH
114
+ PURGE
115
+ WEBSOCKET
116
+ EVENTSOURCE
117
+ )
118
+ end
@@ -0,0 +1,33 @@
1
+ ##
2
+ # Modified from Sinatra
3
+ # Provide flexible configuration for Midori Server
4
+ module Configurable
5
+ # Sets an option to the given value.
6
+ # @param [Symbol] option the name of config
7
+ # @param [Object] value value to the name
8
+ # @param [Boolean] read_only Generate option= method or not
9
+ # @return [nil] nil
10
+ def set(option, value = (not_set = true), read_only = false)
11
+ raise ArgumentError if not_set
12
+
13
+ setter = proc { |val| set option, val }
14
+ getter = proc { value }
15
+
16
+ define_singleton("#{option}=", setter) unless read_only
17
+ define_singleton(option, getter)
18
+ define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
19
+
20
+ self
21
+ end
22
+
23
+ # Dynamically defines a method on settings.
24
+ # @param [String] name method name
25
+ # @param [Proc] content method content
26
+ # @return [nil] nil
27
+ private def define_singleton(name, content = Proc.new)
28
+ singleton_class.class_eval do
29
+ undef_method(name) if method_defined? name
30
+ String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ ##
2
+ # Meta-programming Kernel for Syntactic Sugars
3
+ module Kernel
4
+ # This method is implemented to dynamically generate class with given name and template.
5
+ # Referenced from {Ruby China}[https://ruby-china.org/topics/17382]
6
+ # @param [String] name name of class
7
+ # @param [Class] ancestor class to be inherited
8
+ # @yield inner block to be inserted into class
9
+ # @return [Class] the class defined
10
+ def define_class(name, ancestor = Object)
11
+ Object.const_set(name, Class.new(ancestor))
12
+ Object.const_get(name).class_eval(&Proc.new) if block_given?
13
+ Object.const_get(name)
14
+ end
15
+
16
+ # Define a batch of error handler with given name
17
+ # @param [Array<Symbol>] args names to be defined
18
+ # @return [nil] nil
19
+ # @example
20
+ # define_error(:foo_error, :bar_error)
21
+ # => nil, FooError < StandardError and BarError < StandardError would be defined
22
+ def define_error(*args)
23
+ args.each do |arg|
24
+ class_name = arg.to_s.split('_').collect(&:capitalize).join
25
+ define_class(class_name, StandardError)
26
+ end
27
+ nil
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ ##
2
+ # Meta-programming Proc for Syntactic Sugars
3
+ class Proc
4
+ # Convert [Proc] to [Lambda]
5
+ # @param [Object] instance the context
6
+ # @return [Lambda] Lambda converted
7
+ # @note Converting [Proc] to [Lambda] may have incorrect behaviours on corner cases.
8
+ # @note See {Ruby Language Issues}[https://bugs.ruby-lang.org/issues/7314] for more details.
9
+ def to_lambda(instance = Object.new)
10
+ instance.define_singleton_method(:_, &self)
11
+ instance.method(:_).to_proc
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ ##
2
+ # Meta-programming String for Syntactic Sugars
3
+ class String
4
+ # @param [Integer] color_code ANSI color code
5
+ # @return [String] colored string
6
+ def colorize(color_code)
7
+ "\e[#{color_code}m#{self}\e[0m"
8
+ end
9
+
10
+ # color the string with red color
11
+ def red
12
+ colorize(31)
13
+ end
14
+
15
+ # color the string with green color
16
+ def green
17
+ colorize(32)
18
+ end
19
+
20
+ # color the string with yellow color
21
+ def yellow
22
+ colorize(33)
23
+ end
24
+
25
+ # color the string with blue color
26
+ def blue
27
+ colorize(34)
28
+ end
29
+ end
data/lib/midori/env.rb ADDED
@@ -0,0 +1,18 @@
1
+ module Midori
2
+ # @return [String] midori environment
3
+ def self.env
4
+ ENV['MIDORI_ENV'] || 'development'
5
+ end
6
+ end
7
+
8
+ class String
9
+ # @return [TrueClass | FalseClass] if string is equal to production
10
+ def production?
11
+ self == 'production'
12
+ end
13
+
14
+ #@return [TrueClass | FalseClass] if string is equal to development
15
+ def development?
16
+ self == 'development'
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ ##
2
+ # This class provides methods for EventSource connection instance.
3
+ # @attr [Midori::Connection] connection the connection instance of EventMachine
4
+ class Midori::EventSource
5
+ attr_accessor :connection
6
+
7
+ # Init a EventSource instance with a connection
8
+ # @param [Midori::Connection] connection the connection instance of EventMachine
9
+ def initialize(connection)
10
+ @connection = connection
11
+ end
12
+
13
+ # Send data and close the connection
14
+ # @param [String] data data to be sent
15
+ def send(data)
16
+ raise Midori::Exception::EventSourceTypeError unless data.is_a? String
17
+ @connection.send_data(data.split("\n").map {|str| "data: #{str}\n"}.join + "\n")
18
+ @connection.close_connection_after_writing
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ ##
2
+ # This module store errors to be handled inside Midori
3
+ module Midori::Exception
4
+ # No route matched
5
+ class NotFound < StandardError; end
6
+ # Internal Error
7
+ class InternalError < StandardError; end
8
+ # Midori doesn't support continuous frame of WebSockets yet
9
+ class ContinuousFrame < StandardError; end
10
+ # WebSocket OpCode not defined in RFC standards
11
+ class OpCodeError < StandardError; end
12
+ # Websocket request not masked
13
+ class NotMasked < StandardError; end
14
+ # Websocket frame has ended
15
+ class FrameEnd < StandardError; end
16
+ # Websocket Ping Pong size too large
17
+ class PingPongSizeTooLarge < StandardError; end
18
+ # Not sending String in EventSource
19
+ class EventSourceTypeError < StandardError; end
20
+ # Insert a not middleware class to middleware list
21
+ class MiddlewareError < StandardError; end
22
+ end
@@ -0,0 +1,15 @@
1
+ module Midori
2
+ class << self
3
+ # Return current logger midori is using
4
+ # @return [Logger] the current logger midori is using
5
+ def logger
6
+ @logger ||= ::Logger.new(STDOUT)
7
+ end
8
+
9
+ # Return midori's logger
10
+ # @param [Logger] logger set midori logger
11
+ def logger=(logger)
12
+ @logger = logger
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ ##
2
+ # Ancestor of all middlewares
3
+ class Midori::Middleware
4
+ # Init a middleware
5
+ def initialize
6
+ end
7
+
8
+ # run before processing a request
9
+ # @param [Midori::Request] request raw request
10
+ # @return [Midori::Request] request to be further processed
11
+ def before(request)
12
+ request
13
+ end
14
+
15
+ # run after processing a request
16
+ # @param [Midori::Request] _request raw request
17
+ # @param [Midori::Response] response raw response
18
+ # @return [Midori::Response] response to be further processed
19
+ def after(_request, response)
20
+ response
21
+ end
22
+
23
+ # Dynamically generate a method to use inside router
24
+ # @param [Symbol] name name of the method
25
+ # @yield the block to run
26
+ def self.helper(name, &block)
27
+ Midori::CleanRoom.class_exec do
28
+ define_method(name, &block)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,115 @@
1
+ ##
2
+ # Request class for midori
3
+ # @attr [String] ip client ip address
4
+ # @attr [Integer] port client port
5
+ # @attr [String] protocol protocol version of HTTP request
6
+ # @attr [Symbol] method HTTP method
7
+ # @attr [String] path request path
8
+ # @attr [Hash] query_param parameter parsed from query string
9
+ # @attr [String | nil] query_string request query string
10
+ # @attr [Hash] header request header
11
+ # @attr [String] body request body
12
+ # @attr [Hash] cookie cookie hash coming from request
13
+ # @attr [Boolean] parsed whether the request header parsed
14
+ # @attr [Boolean] body_parsed whether the request body parsed
15
+ # @attr [Hash] params params in the url
16
+ class Midori::Request
17
+ attr_accessor :ip, :port,
18
+ :protocol, :method, :path, :query_params, :query_string,
19
+ :header, :body, :parsed, :body_parsed, :params, :cookie
20
+
21
+ # Init Request
22
+ def initialize
23
+ @header = {}
24
+ @parsed = false
25
+ @body_parsed = false
26
+ @is_websocket = false
27
+ @is_eventsource = false
28
+ @parser = Http::Parser.new
29
+ @params = {}
30
+ @query_params = Hash.new(Array.new)
31
+ @cookie = {}
32
+ @body = ''
33
+ @parser.on_headers_complete = proc do
34
+ @protocol = @parser.http_version
35
+ @method = @parser.http_method
36
+ @path = @parser.request_url
37
+ @header = @parser.headers
38
+
39
+ @query_string = @path.match(/\?(.*?)$/)
40
+ unless @query_string.nil?
41
+ @query_string = @query_string[1]
42
+ @query_params = CGI::parse(@query_string)
43
+ end
44
+
45
+ @cookie = CGI::Cookie.parse(@header['Cookie']) unless @header['Cookie'].nil?
46
+ @path.gsub!(/\?(.*?)$/, '')
47
+ @method = @method.to_sym
48
+ @parsed = true
49
+ :stop
50
+ end
51
+ end
52
+
53
+ # Init an request with String data
54
+ # @param [String] data
55
+ # @return [nil] nil
56
+ def parse(data)
57
+ # Call parser if header not parsed
58
+ if @parsed
59
+ @body += data
60
+ else
61
+ offset = @parser << data
62
+ @body += data[offset..-1] if @parsed
63
+ end
64
+
65
+ # Set body parsed if body reaches content length
66
+ if @parsed && (@header['Content-Length'].to_i || 0) <= @body.bytesize
67
+ @body_parsed = true
68
+ pre_proceed
69
+ end
70
+ nil
71
+ end
72
+
73
+ # Preproceed the request after parsed
74
+ # @return [nil] nil
75
+ def pre_proceed
76
+ # Deal with WebSocket
77
+ if @header['Upgrade'] == 'websocket' && @header['Connection'] == 'Upgrade'
78
+ @method = :WEBSOCKET
79
+ @is_websocket = true
80
+ end
81
+
82
+ # Deal with EventSource
83
+ if @header['Accept'] == 'text/event-stream'
84
+ @method = :EVENTSOURCE
85
+ @is_eventsource = true
86
+ end
87
+
88
+ @method = @method.to_sym
89
+ nil
90
+ end
91
+
92
+ # Syntactic sugar for whether a request header is parsed
93
+ # @return [Boolean] parsed or not
94
+ def parsed?
95
+ @parsed
96
+ end
97
+
98
+ # Syntactic sugar for whether a request body is parsed
99
+ # @return [Boolean] parsed or not
100
+ def body_parsed?
101
+ @body_parsed
102
+ end
103
+
104
+ # Syntactic sugar for whether a request is a websocket request
105
+ # @return [Boolean] websocket or not
106
+ def websocket?
107
+ @is_websocket
108
+ end
109
+
110
+ # Syntactic sugar for whether a request is an eventsource request
111
+ # @return [Boolean] eventsource or not
112
+ def eventsource?
113
+ @is_eventsource
114
+ end
115
+ end
@@ -0,0 +1,34 @@
1
+ ##
2
+ # Class for midori response
3
+ # @attr [String] status HTTP response status
4
+ # @attr [Hash] header HTTP response header
5
+ # @attr [String] body HTTP response body
6
+ class Midori::Response
7
+ attr_accessor :status, :header, :body
8
+
9
+ # @param [Hash] options HTTP response
10
+ # @option options [Integer] code HTTP response code
11
+ # @option options [Hash] header HTTP response header
12
+ # @option options [String] body HTTP response body
13
+ # Init a Response
14
+ def initialize(options = {})
15
+ code = options[:status] || 200
16
+ @status = Midori::Const::STATUS_CODE[code]
17
+ @header = options[:header] || Midori::Const::DEFAULT_HEADER.clone
18
+ @body = options[:body] || ''
19
+ end
20
+
21
+ # Generate header string from hash
22
+ # @return [String] generated string
23
+ def generate_header
24
+ @header.map do |key, value|
25
+ "#{key}: #{value}\r\n"
26
+ end.join
27
+ end
28
+
29
+ # Convert response to raw string
30
+ # @return [String] generated string
31
+ def to_s
32
+ "HTTP/1.1 #{@status}\r\n#{generate_header}\r\n#{@body}"
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ ##
2
+ # Class for Midori route
3
+ # @attr [String] method HTTP method
4
+ # @attr [Regexp] path regex to match
5
+ # @attr [Proc] function what to do after matched
6
+ # @attr [Array<Class>] middlewares middlewares used in the route
7
+ class Midori::Route
8
+ attr_accessor :method, :path, :function, :middlewares
9
+
10
+ # Define a route
11
+ # @param [String] method HTTP method
12
+ # @param [Regexp] path regex to match
13
+ # @param [Proc] function what to do after matched
14
+ def initialize(method, path, function)
15
+ @method = method
16
+ @path = path
17
+ @function = function
18
+ @middlewares = []
19
+ end
20
+ end
@@ -0,0 +1,63 @@
1
+ ##
2
+ # Abstract runner class to control instance of Midori Server
3
+ # @attr [String] bind the address to bind
4
+ # @attr [Integer] port the port to bind
5
+ # @attr [Logger] logger midori logger
6
+ class Midori::Runner
7
+ attr_reader :bind, :port, :logger
8
+
9
+ # Define status of a runner
10
+ # @param [Class] api inherited from [Midori::API]
11
+ # @param [Class] configure inherited from [Midori::Configure]
12
+ def initialize(api, configure = Midori::Configure)
13
+ @logger = configure.logger
14
+ Midori.logger = configure.logger
15
+ @bind = configure.bind
16
+ @port = configure.port
17
+ @api = (api.is_a?Midori::APIEngine) ? api : Midori::APIEngine.new(api, configure.route_type)
18
+ @before = configure.before
19
+ end
20
+
21
+ # Get Midori server whether running
22
+ # @return [Boolean] [true] running
23
+ # @return [Boolean] [false] not running
24
+ def running?
25
+ !!@server
26
+ end
27
+
28
+ # Start the Midori server
29
+ # @note This is an async method, but no callback
30
+ def start
31
+ return false if running? || EventLoop.running?
32
+ @logger.info "Midori #{Midori::VERSION} is now running on #{bind}:#{port}".blue
33
+ @server = TCPServer.new(@bind, @port)
34
+ EventLoop.register(@server, :r) do |monitor|
35
+ socket = monitor.io.accept_nonblock
36
+ connection = Midori::Connection.new(socket)
37
+ connection.server_initialize(@api, @logger)
38
+ end
39
+ async_fiber(Fiber.new do
40
+ @before.call
41
+ end)
42
+ EventLoop.start
43
+ nil
44
+ end
45
+
46
+ # Stop the Midori server
47
+ # @note This is an async method, but no callback
48
+ # @return [Boolean] [true] stop successfully
49
+ # @return [Boolean] [false] nothing to stop
50
+ def stop
51
+ if running?
52
+ @logger.info 'Stopping Midori'.blue
53
+ EventLoop.deregister @server
54
+ @server.close
55
+ @server = nil
56
+ EventLoop.stop
57
+ true
58
+ else
59
+ @logger.error 'Midori Server has NOT been started'.red
60
+ false
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,46 @@
1
+ ##
2
+ # Sandbox for global error capture
3
+ class Midori::Sandbox
4
+ class << self
5
+ def class_initialize
6
+ @handlers = Hash.new
7
+ @handlers[Midori::Exception::InternalError] = proc {|e| Midori::Response.new(status: 500, body: "#{e.inspect} #{e.backtrace}")}
8
+ @handlers[Midori::Exception::NotFound] = proc {|_e| Midori::Response.new(status: 404, body: '404 Not Found')}
9
+ end
10
+
11
+ # Add a rule to Sandbox
12
+ # @param [Class] class_name the class to capture
13
+ # @param [Proc] block what to do when captured
14
+ # @return [nil] nil
15
+ def add_rule(class_name, block)
16
+ @handlers[class_name] = block
17
+ nil
18
+ end
19
+
20
+ # Detect what to run with given error
21
+ # @param [StandardError] error the error captured
22
+ # @return [nil] nil
23
+ def capture(error)
24
+ if @handlers[error.class].nil?
25
+ @handlers[Midori::Exception::InternalError].call(error)
26
+ else
27
+ @handlers[error.class].call(error)
28
+ end
29
+ end
30
+
31
+ # Run sandbox inside given clean room
32
+ # @param [Midori::CleanRoom] clean_room Clean room to run
33
+ # @param [Proc] function the block to run
34
+ # @return [nil] nil
35
+ def run(clean_room, function, *args)
36
+ begin
37
+ function.to_lambda(clean_room).call(*args)
38
+ rescue StandardError => e
39
+ capture(e)
40
+ end
41
+ end
42
+ end
43
+
44
+ private_class_method :class_initialize
45
+ class_initialize
46
+ end