plezi 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/CHANGELOG.md +450 -0
- data/Gemfile +4 -0
- data/KNOWN_ISSUES.md +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +341 -0
- data/Rakefile +2 -0
- data/TODO.md +19 -0
- data/bin/plezi +301 -0
- data/lib/plezi.rb +125 -0
- data/lib/plezi/base/cache.rb +77 -0
- data/lib/plezi/base/connections.rb +33 -0
- data/lib/plezi/base/dsl.rb +177 -0
- data/lib/plezi/base/engine.rb +85 -0
- data/lib/plezi/base/events.rb +84 -0
- data/lib/plezi/base/io_reactor.rb +41 -0
- data/lib/plezi/base/logging.rb +62 -0
- data/lib/plezi/base/rack_app.rb +89 -0
- data/lib/plezi/base/services.rb +57 -0
- data/lib/plezi/base/timers.rb +71 -0
- data/lib/plezi/handlers/controller_magic.rb +383 -0
- data/lib/plezi/handlers/http_echo.rb +27 -0
- data/lib/plezi/handlers/http_host.rb +215 -0
- data/lib/plezi/handlers/http_router.rb +69 -0
- data/lib/plezi/handlers/magic_helpers.rb +43 -0
- data/lib/plezi/handlers/route.rb +272 -0
- data/lib/plezi/handlers/stubs.rb +143 -0
- data/lib/plezi/server/README.md +33 -0
- data/lib/plezi/server/helpers/http.rb +169 -0
- data/lib/plezi/server/helpers/mime_types.rb +999 -0
- data/lib/plezi/server/protocols/http_protocol.rb +318 -0
- data/lib/plezi/server/protocols/http_request.rb +133 -0
- data/lib/plezi/server/protocols/http_response.rb +294 -0
- data/lib/plezi/server/protocols/websocket.rb +208 -0
- data/lib/plezi/server/protocols/ws_response.rb +92 -0
- data/lib/plezi/server/services/basic_service.rb +224 -0
- data/lib/plezi/server/services/no_service.rb +196 -0
- data/lib/plezi/server/services/ssl_service.rb +193 -0
- data/lib/plezi/version.rb +3 -0
- data/plezi.gemspec +26 -0
- data/resources/404.erb +68 -0
- data/resources/404.haml +64 -0
- data/resources/404.html +67 -0
- data/resources/404.slim +63 -0
- data/resources/500.erb +68 -0
- data/resources/500.haml +63 -0
- data/resources/500.html +67 -0
- data/resources/500.slim +63 -0
- data/resources/Gemfile +85 -0
- data/resources/anorexic_gray.png +0 -0
- data/resources/anorexic_websockets.html +47 -0
- data/resources/code.rb +8 -0
- data/resources/config.ru +39 -0
- data/resources/controller.rb +139 -0
- data/resources/db_ac_config.rb +58 -0
- data/resources/db_dm_config.rb +51 -0
- data/resources/db_sequel_config.rb +42 -0
- data/resources/en.yml +204 -0
- data/resources/environment.rb +41 -0
- data/resources/haml_config.rb +6 -0
- data/resources/i18n_config.rb +14 -0
- data/resources/rakefile.rb +22 -0
- data/resources/redis_config.rb +35 -0
- data/resources/routes.rb +26 -0
- data/resources/welcome_page.html +72 -0
- data/websocket chatroom.md +639 -0
- metadata +141 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
|
2
|
+
module Plezi
|
3
|
+
|
4
|
+
module_function
|
5
|
+
|
6
|
+
#######################
|
7
|
+
## Events (Callbacks) / Multi-tasking Platform
|
8
|
+
|
9
|
+
# DANGER ZONE - Plezi Engine. an Array containing all the shutdown callbacks that need to be called.
|
10
|
+
SHUTDOWN_CALLBACKS = []
|
11
|
+
# DANGER ZONE - Plezi Engine. an Array containing all the current events.
|
12
|
+
EVENTS = []
|
13
|
+
# DANGER ZONE - Plezi Engine. the Mutex locker for the event machine.
|
14
|
+
LOCKER = Mutex.new
|
15
|
+
|
16
|
+
# returns true if there are any unhandled events
|
17
|
+
def events?
|
18
|
+
LOCKER.synchronize {!EVENTS.empty?}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public API. pushes an event to the event's stack
|
22
|
+
#
|
23
|
+
# accepts:
|
24
|
+
# handler:: an object that answers to `call`, usually a Proc or a method.
|
25
|
+
# *arg:: any arguments that will be passed to the handler's `call` method.
|
26
|
+
#
|
27
|
+
# if a block is passed along, it will be used as a callback: the block will be called with the values returned by the handler's `call` method.
|
28
|
+
def push_event handler, *args, &block
|
29
|
+
if block
|
30
|
+
LOCKER.synchronize {EVENTS << [(Proc.new {|a| Plezi.push_event block, handler.call(*a)} ), args]}
|
31
|
+
else
|
32
|
+
LOCKER.synchronize {EVENTS << [handler, args]}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Public API. creates an asynchronous call to a method, with an optional callback:
|
37
|
+
# demo use:
|
38
|
+
# `callback( Kernel, :sleep, 1 ) { puts "this is a demo" }`
|
39
|
+
# callback sets an asynchronous method call with a callback.
|
40
|
+
#
|
41
|
+
# paramaters:
|
42
|
+
# object:: the object holding the method to be called (use `Kernel` for global methods).
|
43
|
+
# method:: the method's name (Symbol). this is the method that will be called.
|
44
|
+
# *arguments:: any additional arguments that should be sent to the method (the main method, not the callback).
|
45
|
+
#
|
46
|
+
# this method also accepts an optional block (the callback) that will be called once the main method has completed.
|
47
|
+
# the block will recieve the method's returned value.
|
48
|
+
#
|
49
|
+
# i.e.
|
50
|
+
# puts 'while we wait for my work to complete, can you tell me your name?'
|
51
|
+
# Plezi.callback(STDIO, :gets) do |name|
|
52
|
+
# puts "thank you, #{name}. I'm working on your request as we speak."
|
53
|
+
# end
|
54
|
+
# # do more work without waiting for the chat to start nor complete.
|
55
|
+
def callback object, method, *args, &block
|
56
|
+
push_event object.method(method), *args, &block
|
57
|
+
end
|
58
|
+
|
59
|
+
# Public API. adds a callback to be called once the services were shut down. see: callback for more info.
|
60
|
+
def on_shutdown object=nil, method=nil, *args, &block
|
61
|
+
if block && !object && !method
|
62
|
+
LOCKER.synchronize {SHUTDOWN_CALLBACKS << [block, args]}
|
63
|
+
elsif block
|
64
|
+
LOCKER.synchronize {SHUTDOWN_CALLBACKS << [(Proc.new {|*a| block.call(object.method(method).call(*a))} ), args]}
|
65
|
+
elsif object && method
|
66
|
+
LOCKER.synchronize {SHUTDOWN_CALLBACKS << [object.method(method), args]}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Plezi Engine, DO NOT CALL. pulls an event from the event's stack and processes it.
|
71
|
+
def fire_event
|
72
|
+
event = LOCKER.synchronize {EVENTS.shift}
|
73
|
+
return false unless event
|
74
|
+
begin
|
75
|
+
event[0].call(*event[1])
|
76
|
+
rescue OpenSSL::SSL::SSLError => e
|
77
|
+
warn "SSL Bump - SSL Certificate refused?"
|
78
|
+
rescue Exception => e
|
79
|
+
raise if e.is_a?(SignalException) || e.is_a?(SystemExit)
|
80
|
+
error e
|
81
|
+
end
|
82
|
+
true
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
module Plezi
|
3
|
+
|
4
|
+
module_function
|
5
|
+
|
6
|
+
|
7
|
+
# DANGER ZONE - Plezi Engine. the io reactor mutex
|
8
|
+
IO_LOCKER = Mutex.new
|
9
|
+
|
10
|
+
# Plezi Engine, DO NOT CALL. waits on IO and pushes events. all threads hang while reactor is active (unless events are already 'in the pipe'.)
|
11
|
+
def io_reactor
|
12
|
+
IO_LOCKER.synchronize do
|
13
|
+
return false unless EVENTS.empty?
|
14
|
+
united = SERVICES.keys + IO_CONNECTION_DIC.keys
|
15
|
+
return false if united.empty?
|
16
|
+
io_r = (IO.select(united, nil, united, idle_sleep) ) #rescue false)
|
17
|
+
if io_r
|
18
|
+
io_r[0].each do |io|
|
19
|
+
if SERVICES[io]
|
20
|
+
begin
|
21
|
+
connection = io.accept_nonblock
|
22
|
+
callback Plezi, :add_connection, connection, SERVICES[io]
|
23
|
+
rescue Errno::EWOULDBLOCK => e
|
24
|
+
|
25
|
+
rescue Exception => e
|
26
|
+
error e
|
27
|
+
# SERVICES.delete s if s.closed?
|
28
|
+
end
|
29
|
+
elsif IO_CONNECTION_DIC[io]
|
30
|
+
callback(IO_CONNECTION_DIC[io], :on_message)
|
31
|
+
else
|
32
|
+
IO_CONNECTION_DIC.delete(io)
|
33
|
+
SERVICES.delete(io)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
io_r[2].each { |io| (IO_CONNECTION_DIC.delete(io) || SERVICES.delete(io)).close rescue true }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
true
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
module Plezi
|
3
|
+
|
4
|
+
#######################
|
5
|
+
## Logging
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# the logger object
|
9
|
+
@logger = ::Logger.new(STDOUT)
|
10
|
+
|
11
|
+
# gets the active logger
|
12
|
+
def logger
|
13
|
+
@logger
|
14
|
+
end
|
15
|
+
# gets the active STDOUT copy, if exists
|
16
|
+
def logger_copy
|
17
|
+
@copy_to_stdout
|
18
|
+
end
|
19
|
+
|
20
|
+
# create and set the logger object. accepts:
|
21
|
+
# log_file:: a log file name to be used for logging
|
22
|
+
# copy_to_stdout:: if false, log will only log to file. defaults to true.
|
23
|
+
def create_logger log_file = STDOUT, copy_to_stdout = false
|
24
|
+
@copy_to_stdout = false
|
25
|
+
@copy_to_stdout = ::Logger.new(STDOUT) if copy_to_stdout
|
26
|
+
@logger = ::Logger.new(log_file)
|
27
|
+
@logger
|
28
|
+
end
|
29
|
+
alias :set_logger :create_logger
|
30
|
+
|
31
|
+
# writes a raw line to the log\
|
32
|
+
def log_raw line
|
33
|
+
@logger << line
|
34
|
+
@copy_to_stdout << line if @copy_to_stdout
|
35
|
+
end
|
36
|
+
|
37
|
+
# logs info
|
38
|
+
def log line
|
39
|
+
@logger.info line
|
40
|
+
@copy_to_stdout.info line if @copy_to_stdout
|
41
|
+
end
|
42
|
+
# logs info
|
43
|
+
def info line
|
44
|
+
@logger.info line
|
45
|
+
@copy_to_stdout.info line if @copy_to_stdout
|
46
|
+
end
|
47
|
+
# logs warning
|
48
|
+
def warn line
|
49
|
+
@logger.warn line
|
50
|
+
@copy_to_stdout.warn line if @copy_to_stdout
|
51
|
+
end
|
52
|
+
# logs errors
|
53
|
+
def error line
|
54
|
+
@logger.error line
|
55
|
+
@copy_to_stdout.error line if @copy_to_stdout
|
56
|
+
end
|
57
|
+
# logs a fatal error
|
58
|
+
def fatal line
|
59
|
+
@logger.fatal line
|
60
|
+
@copy_to_stdout.fatal line if @copy_to_stdout
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
|
2
|
+
module Plezi
|
3
|
+
|
4
|
+
# Rack application model support
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# todo: move falsh into the special cookie class...?
|
8
|
+
|
9
|
+
|
10
|
+
# Plezi dresses up for Rack - this is a watered down version missing some features (such as flash and WebSockets).
|
11
|
+
# a full featured Plezi app, with WebSockets, requires the use of the Plezi server
|
12
|
+
# (the built-in server)
|
13
|
+
def call env
|
14
|
+
raise "No Plezi Services" unless Plezi::SERVICES[0]
|
15
|
+
Object.const_set('PLEZI_ON_RACK', true) unless defined? PLEZI_ON_RACK
|
16
|
+
|
17
|
+
# re-encode to utf-8, as it's all BINARY encoding at first
|
18
|
+
env["rack.input"].rewind
|
19
|
+
env['rack.input'] = StringIO.new env["rack.input"].read.encode("utf-8", "binary", invalid: :replace, undef: :replace, replace: '')
|
20
|
+
env.each do |k, v|
|
21
|
+
if k.to_s.match /^[A-Z]/
|
22
|
+
if v.is_a?(String) && !v.frozen?
|
23
|
+
v.force_encoding("binary").encode!("utf-8", "binary", invalid: :replace, undef: :replace, replace: '') unless v.force_encoding("utf-8").valid_encoding?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
# re-key params
|
28
|
+
# new_params = {}
|
29
|
+
# env[:params].each {|k,v| HTTP.add_param_to_hash k, v, new_params}
|
30
|
+
# env[:params] = new_params
|
31
|
+
|
32
|
+
# make hashes magical
|
33
|
+
make_hash_accept_symbols(env)
|
34
|
+
|
35
|
+
# use Plezi Cookies
|
36
|
+
env["rack.request.cookie_string"] = env["HTTP_COOKIE"]
|
37
|
+
env["rack.request.cookie_hash"] = Plezi::Cookies.new.update(env["rack.request.cookie_hash"] || {})
|
38
|
+
|
39
|
+
# chomp path
|
40
|
+
env["PATH_INFO"].chomp! '/'
|
41
|
+
|
42
|
+
# get response
|
43
|
+
response = Plezi::SERVICES[0][1][:handler].call env
|
44
|
+
|
45
|
+
return response if response.is_a?(Array)
|
46
|
+
|
47
|
+
response.finish
|
48
|
+
response.fix_headers
|
49
|
+
headers = response.headers
|
50
|
+
# set cookie headers
|
51
|
+
headers.delete 'transfer-encoding'
|
52
|
+
headers.delete 'connection'
|
53
|
+
unless response.cookies.empty?
|
54
|
+
headers["Set-Cookie"] = []
|
55
|
+
response.cookies.each {|k,v| headers["Set-Cookie"] << ("#{k.to_s}=#{v.to_s}")}
|
56
|
+
end
|
57
|
+
[response.status, headers, response.body]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# # rack code to set cookie headers
|
62
|
+
# # File actionpack/lib/action_controller/vendor/rack-1.0/rack/response.rb, line 56
|
63
|
+
# def set_cookie(key, value)
|
64
|
+
# case value
|
65
|
+
# when Hash
|
66
|
+
# domain = "; domain=" + value[:domain] if value[:domain]
|
67
|
+
# path = "; path=" + value[:path] if value[:path]
|
68
|
+
# # According to RFC 2109, we need dashes here.
|
69
|
+
# # N.B.: cgi.rb uses spaces...
|
70
|
+
# expires = "; expires=" + value[:expires].clone.gmtime.
|
71
|
+
# strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
|
72
|
+
# secure = "; secure" if value[:secure]
|
73
|
+
# httponly = "; HttpOnly" if value[:httponly]
|
74
|
+
# value = value[:value]
|
75
|
+
# end
|
76
|
+
# value = [value] unless Array === value
|
77
|
+
# cookie = Utils.escape(key) + "=" +
|
78
|
+
# value.map { |v| Utils.escape v }.join("&") +
|
79
|
+
# "#{domain}#{path}#{expires}#{secure}#{httponly}"
|
80
|
+
|
81
|
+
# case self["Set-Cookie"]
|
82
|
+
# when Array
|
83
|
+
# self["Set-Cookie"] << cookie
|
84
|
+
# when String
|
85
|
+
# self["Set-Cookie"] = [self["Set-Cookie"], cookie]
|
86
|
+
# when nil
|
87
|
+
# self["Set-Cookie"] = cookie
|
88
|
+
# end
|
89
|
+
# end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
module Plezi
|
3
|
+
|
4
|
+
module_function
|
5
|
+
|
6
|
+
#######################
|
7
|
+
## Services pooling and calling
|
8
|
+
|
9
|
+
# DANGER ZONE - Plezi Engine. the services store
|
10
|
+
SERVICES = {}
|
11
|
+
# DANGER ZONE - Plezi Engine. the services mutex
|
12
|
+
S_LOCKER = Mutex.new
|
13
|
+
|
14
|
+
# public API to add a service to the framework.
|
15
|
+
# accepts:
|
16
|
+
# port:: port number
|
17
|
+
# parameters:: a hash of paramaters that are passed on to the service for handling (and from there, service dependent, to the protocol and/or handler).
|
18
|
+
#
|
19
|
+
# parameters are any of the following:
|
20
|
+
# host:: the host name. defaults to any host not explicitly defined (a catch-all).
|
21
|
+
# alias:: a String or an Array of Strings which represent alternative host names (i.e. `alias: ["admin.google.com", "admin.gmail.com"]`).
|
22
|
+
# root:: the public root folder. if this is defined, static files will be served from the location.
|
23
|
+
# assets:: the assets root folder. defaults to nil (no assets support). if the path is defined, assets will be served from `/assets/...` (or the public_asset path defined) before any static files. assets will not be served if the file in the /public/assets folder if up to date (a rendering attempt will be made for systems that allow file writing).
|
24
|
+
# assets_public:: the assets public uri location (uri format, NOT a file path). defaults to `/assets`. assets will be saved (or rendered) to the assets public folder and served as static files.
|
25
|
+
# assets_callback:: a method that accepts one parameters: `request` and renders any custom assets. the method should return `false` unless it has created a response object (`response = Plezi::HTTPResponse.new(request)`) and sent a response to the client using `response.finish`.
|
26
|
+
# save_assets:: saves the rendered assets to the filesystem, under the public folder. defaults to false.
|
27
|
+
# templates:: the templates root folder. defaults to nil (no template support). templates can be rendered by a Controller class, using the `render` method.
|
28
|
+
# ssl:: if true, an SSL service will be attempted. if no certificate is defined, an attempt will be made to create a self signed certificate.
|
29
|
+
# ssl_key:: the public key for the SSL service.
|
30
|
+
# ssl_cert:: the certificate for the SSL service.
|
31
|
+
#
|
32
|
+
#
|
33
|
+
# assets:
|
34
|
+
#
|
35
|
+
# assets support will render `.sass`, `.scss` and `.coffee` and save them as local files (`.css`, `.css`, and `.js` respectively) before sending them as static files. if it is impossible to write the files, they will be rendered dynamically for every request (it would be better to render them before-hand).
|
36
|
+
#
|
37
|
+
# templates:
|
38
|
+
#
|
39
|
+
# templates can be either an ERB file on a Haml file.
|
40
|
+
#
|
41
|
+
def add_service port, paramaters = {}
|
42
|
+
paramaters[:port] ||= port
|
43
|
+
paramaters[:service_type] ||= ( paramaters[:ssl] ? SSLService : BasicService)
|
44
|
+
service = nil
|
45
|
+
service = paramaters[:service_type].create_service(port, paramaters) unless ( defined?(BUILDING_PLEZI_TEMPLATE) || defined?(PLEZI_ON_RACK) )
|
46
|
+
S_LOCKER.synchronize {SERVICES[service] = paramaters}
|
47
|
+
info "Started listening on port #{port}."
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
# Plezi Engine, DO NOT CALL. stops all services - active connection will remain open until completion.
|
52
|
+
def stop_services
|
53
|
+
info 'Stopping services'
|
54
|
+
S_LOCKER.synchronize {SERVICES.each {|s, p| s.close rescue true; info "Stoped listening on port #{p[:port]}"}; SERVICES.clear }
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
|
2
|
+
module Plezi
|
3
|
+
|
4
|
+
module_function
|
5
|
+
|
6
|
+
#######################
|
7
|
+
## Timed Events / Multi-tasking
|
8
|
+
|
9
|
+
# DANGER ZONE - Plezi Engine. an Array containing all the current events.
|
10
|
+
TIMERS = []
|
11
|
+
# DANGER ZONE - Plezi Engine. the Mutex locker for the event machine.
|
12
|
+
T_LOCKER = Mutex.new
|
13
|
+
|
14
|
+
# This class is used by Plezi to hold events and push them into the events stack when the time comes.
|
15
|
+
class TimedEvent
|
16
|
+
|
17
|
+
def initialize seconds, repeat, handler, args, block
|
18
|
+
@time = Time.now + seconds
|
19
|
+
@seconds, @repeat, @handler, @args, @block = seconds, repeat, handler, args, block
|
20
|
+
end
|
21
|
+
|
22
|
+
def done?
|
23
|
+
return false unless @time <= Time.now
|
24
|
+
Plezi.push_event @handler, *@args, &@block
|
25
|
+
return true unless @repeat
|
26
|
+
@time = Time.now + @seconds
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop_repeat
|
31
|
+
@repeat = false
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :timed
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns true if there are any unhandled events
|
38
|
+
def timers?
|
39
|
+
T_LOCKER.synchronize {!TIMERS.empty?}
|
40
|
+
end
|
41
|
+
|
42
|
+
# pushes a timed event to the timers's stack
|
43
|
+
#
|
44
|
+
# accepts:
|
45
|
+
# seconds:: the minimal amount of seconds to wait before calling the handler's `call` method.
|
46
|
+
# handler:: an object that answers to `call`, usually a Proc or a method.
|
47
|
+
# *arg:: any arguments that will be passed to the handler's `call` method.
|
48
|
+
#
|
49
|
+
# if a block is passed along, it will be used as a callback: the block will be called with the values returned by the handler's `call` method.
|
50
|
+
def run_after seconds, handler, *args, &block
|
51
|
+
T_LOCKER.synchronize {TIMERS << TimedEvent.new(seconds, false, handler, args, block); TIMERS.last}
|
52
|
+
end
|
53
|
+
|
54
|
+
# pushes a repeated timed event to the timers's stack
|
55
|
+
#
|
56
|
+
# accepts:
|
57
|
+
# seconds:: the minimal amount of seconds to wait before calling the handler's `call` method.
|
58
|
+
# handler:: an object that answers to `call`, usually a Proc or a method.
|
59
|
+
# *arg:: any arguments that will be passed to the handler's `call` method.
|
60
|
+
#
|
61
|
+
# if a block is passed along, it will be used as a callback: the block will be called with the values returned by the handler's `call` method.
|
62
|
+
def run_every seconds, handler, *args, &block
|
63
|
+
T_LOCKER.synchronize {TIMERS << TimedEvent.new(seconds, true, handler, args, block); TIMERS.last}
|
64
|
+
end
|
65
|
+
|
66
|
+
# DANGER ZONE - Used by the Plezi engine to review timed events and push them to the event stack
|
67
|
+
def fire_timers
|
68
|
+
return false if T_LOCKER.locked?
|
69
|
+
T_LOCKER.synchronize { TIMERS.delete_if {|t| t.done? } }
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,383 @@
|
|
1
|
+
module Plezi
|
2
|
+
|
3
|
+
# the methods defined in this module will be injected into the Controller class passed to
|
4
|
+
# Plezi (using the `route` or `shared_route` commands), and will be available
|
5
|
+
# for the controller to use within it's methods.
|
6
|
+
#
|
7
|
+
# for some reason, the documentation ignores the following additional attributes, which are listed here:
|
8
|
+
#
|
9
|
+
# request:: the HTTPRequest object containing all the data from the HTTP request. If a WebSocket connection was established, the `request` object will continue to contain the HTTP request establishing the connection (cookies, parameters sent and other information).
|
10
|
+
# params:: any parameters sent with the request (short-cut for `request.params`), will contain any GET or POST form data sent (including file upload and JSON format support).
|
11
|
+
# cookies:: a cookie-jar to get and set cookies (set: `cookie\[:name] = data` or get: `cookie\[:name]`). Cookies and some other data must be set BEFORE the response's headers are sent.
|
12
|
+
# flash:: a temporary cookie-jar, good for one request. this is a short-cut for the `response.flash` which handles this magical cookie style.
|
13
|
+
# response:: the HTTPResponse **OR** the WSResponse object that formats the response and sends it. use `response << data`. This object can be used to send partial data (such as headers, or partial html content) in blocking mode as well as sending data in the default non-blocking mode.
|
14
|
+
# host_params:: a copy of the parameters used to create the host and service which accepted the request and created this instance of the controller class.
|
15
|
+
#
|
16
|
+
module ControllerMagic
|
17
|
+
def self.included base
|
18
|
+
base.send :include, InstanceMethods
|
19
|
+
base.extend ClassMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
module_function
|
24
|
+
public
|
25
|
+
|
26
|
+
# the request object, class: HTTPRequest.
|
27
|
+
attr_reader :request
|
28
|
+
|
29
|
+
# the ::params variable contains all the paramaters set by the request (/path?locale=he => params["locale"] == "he").
|
30
|
+
attr_reader :params
|
31
|
+
|
32
|
+
# a cookie-jar to get and set cookies (set: `cookie\[:name] = data` or get: `cookie\[:name]`).
|
33
|
+
#
|
34
|
+
# Cookies and some other data must be set BEFORE the response's headers are sent.
|
35
|
+
attr_reader :cookies
|
36
|
+
|
37
|
+
# the HTTPResponse **OR** the WSResponse object that formats the response and sends it. use `response << data`. This object can be used to send partial data (such as headers, or partial html content) in blocking mode as well as sending data in the default non-blocking mode.
|
38
|
+
attr_accessor :response
|
39
|
+
|
40
|
+
# the ::flash is a little bit of a magic hash that sets and reads temporary cookies.
|
41
|
+
# these cookies will live for one successful request to a Controller and will then be removed.
|
42
|
+
attr_reader :flash
|
43
|
+
|
44
|
+
# the parameters used to create the host (the parameters passed to the `listen` / `add_service` call).
|
45
|
+
attr_reader :host_params
|
46
|
+
|
47
|
+
# a unique UUID to identify the object - used to make sure Radis broadcasts don't triger the
|
48
|
+
# boadcasting object's event.
|
49
|
+
attr_reader :uuid
|
50
|
+
|
51
|
+
# checks whether this instance accepts broadcasts (WebSocket instances).
|
52
|
+
def accepts_broadcast?
|
53
|
+
@_accepts_broadcast
|
54
|
+
end
|
55
|
+
|
56
|
+
# this method does two things.
|
57
|
+
#
|
58
|
+
# 1. sets redirection headers for the response.
|
59
|
+
# 2. sets the `flash` object (short-time cookies) with all the values passed except the :status value.
|
60
|
+
#
|
61
|
+
# use:
|
62
|
+
# redirect_to 'http://google.com', notice: "foo", status: 302
|
63
|
+
# # => redirects to 'http://google.com' with status 302 and adds notice: "foo" to the flash
|
64
|
+
# or simply:
|
65
|
+
# redirect_to 'http://google.com'
|
66
|
+
# # => redirects to 'http://google.com' with status 302 (default status)
|
67
|
+
#
|
68
|
+
# if the url is a symbol, the method will try to format it into a correct url, replacing any
|
69
|
+
# underscores ('_') with a backslash ('/').
|
70
|
+
#
|
71
|
+
# if the url is an empty string, the method will try to format it into a correct url
|
72
|
+
# representing the index of the application (http://server/)
|
73
|
+
#
|
74
|
+
def redirect_to url, options = {}
|
75
|
+
return super *[] if defined? super
|
76
|
+
raise 'Cannot redirect after headers were sent' if response.headers_sent?
|
77
|
+
url = "#{request.base_url}/#{url.to_s.gsub('_', '/')}" if url.is_a?(Symbol) || ( url.is_a?(String) && url.empty? ) || url.nil?
|
78
|
+
# redirect
|
79
|
+
response.status = options.delete(:status) || 302
|
80
|
+
response['Location'] = url
|
81
|
+
response['content-length'] ||= 0
|
82
|
+
flash.update options
|
83
|
+
response.finish
|
84
|
+
true
|
85
|
+
end
|
86
|
+
|
87
|
+
# this method adds data to be sent.
|
88
|
+
#
|
89
|
+
# this is usful for sending 'attachments' (data to be downloaded) rather then
|
90
|
+
# a regular response.
|
91
|
+
#
|
92
|
+
# this is also usful for offering a file name for the browser to "save as".
|
93
|
+
#
|
94
|
+
# it accepts:
|
95
|
+
# data:: the data to be sent
|
96
|
+
# options:: a hash of any of the options listed furtheron.
|
97
|
+
#
|
98
|
+
# the :symbol=>value options are:
|
99
|
+
# type:: the type of the data to be sent. defaults to empty. if :filename is supplied, an attempt to guess will be made.
|
100
|
+
# inline:: sets the data to be sent an an inline object (to be viewed rather then downloaded). defaults to false.
|
101
|
+
# filename:: sets a filename for the browser to "save as". defaults to empty.
|
102
|
+
#
|
103
|
+
def send_data data, options = {}
|
104
|
+
raise 'Cannot use "send_data" after headers were sent' if response.headers_sent?
|
105
|
+
# write data to response object
|
106
|
+
response << data
|
107
|
+
|
108
|
+
# set headers
|
109
|
+
content_disposition = "attachment"
|
110
|
+
|
111
|
+
options[:type] ||= MimeTypeHelper::MIME_DICTIONARY[::File.extname(options[:filename])] if options[:filename]
|
112
|
+
|
113
|
+
if options[:type]
|
114
|
+
response["content-type"] = options[:type]
|
115
|
+
options.delete :type
|
116
|
+
end
|
117
|
+
if options[:inline]
|
118
|
+
content_disposition = "inline"
|
119
|
+
options.delete :inline
|
120
|
+
end
|
121
|
+
if options[:attachment]
|
122
|
+
options.delete :attachment
|
123
|
+
end
|
124
|
+
if options[:filename]
|
125
|
+
content_disposition << "; filename=#{options[:filename]}"
|
126
|
+
options.delete :filename
|
127
|
+
end
|
128
|
+
response["content-length"] = data.bytesize rescue true
|
129
|
+
response["content-disposition"] = content_disposition
|
130
|
+
response.finish
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
# renders a template file (.erb/.haml) or an html file (.html) to text
|
135
|
+
# for example, to render the file `body.html.haml` with the layout `main_layout.html.haml`:
|
136
|
+
# render :body, layout: :main_layout
|
137
|
+
#
|
138
|
+
# or, for example, to render the file `json.js.haml`
|
139
|
+
# render :json, type: 'js'
|
140
|
+
#
|
141
|
+
# or, for example, to render the file `template.haml`
|
142
|
+
# render :template, type: ''
|
143
|
+
#
|
144
|
+
# template:: a Symbol for the template to be used.
|
145
|
+
# options:: a Hash for any options such as `:layout` or `locale`.
|
146
|
+
# block:: an optional block, in case the template has `yield`, the block will be passed on to the template and it's value will be used inplace of the yield statement.
|
147
|
+
#
|
148
|
+
# options aceept the following keys:
|
149
|
+
# type:: the types for the `:layout' and 'template'. can be any extention, such as `"json"`. defaults to `"html"`.
|
150
|
+
# layout:: a layout template that has at least one `yield` statement where the template will be rendered.
|
151
|
+
# locale:: the I18n locale for the render. (defaults to params[:locale]) - only if the I18n gem namespace is defined (`require 'i18n'`).
|
152
|
+
#
|
153
|
+
# if template is a string, it will assume the string is an
|
154
|
+
# absolute path to a template file. it will NOT search for the template but might raise exceptions.
|
155
|
+
#
|
156
|
+
# if the template is a symbol, the '_' caracters will be used to destinguish sub-folders (NOT a partial template).
|
157
|
+
#
|
158
|
+
# returns false if the template or layout files cannot be found.
|
159
|
+
def render template, options = {}, &block
|
160
|
+
# make sure templates are enabled
|
161
|
+
return false if host_params[:templates].nil?
|
162
|
+
# render layout by recursion, if exists
|
163
|
+
(return render(options.delete(:layout), options) { render template, options, &block }) if options[:layout]
|
164
|
+
# set up defaults
|
165
|
+
options[:type] ||= 'html'
|
166
|
+
options[:locale] ||= params[:locale].to_sym if params[:locale]
|
167
|
+
# options[:locals] ||= {}
|
168
|
+
I18n.locale = options[:locale] if defined?(I18n) && options[:locale]
|
169
|
+
# find template and create template object
|
170
|
+
filename = template.is_a?(String) ? File.join( host_params[:templates].to_s, template) : (File.join( host_params[:templates].to_s, *template.to_s.split('_')) + (options[:type].empty? ? '': ".#{options[:type]}") + '.slim')
|
171
|
+
return ( Plezi.cache_needs_update?(filename) ? Plezi.cache_data( filename, ( Slim::Template.new() { IO.read filename } ) ) : (Plezi.get_cached filename) ).render(self, &block) if defined?(::Slim) && Plezi.file_exists?(filename)
|
172
|
+
filename.gsub! /\.slim$/, '.haml'
|
173
|
+
return ( Plezi.cache_needs_update?(filename) ? Plezi.cache_data( filename, ( Haml::Engine.new( IO.read(filename) ) ) ) : (Plezi.get_cached filename) ).render(self, &block) if defined?(::Haml) && Plezi.file_exists?(filename)
|
174
|
+
filename.gsub! /\.haml$/, '.erb'
|
175
|
+
return ( Plezi.cache_needs_update?(filename) ? Plezi.cache_data( filename, ( ERB.new( IO.read(filename) ) ) ) : (Plezi.get_cached filename) ).result(binding, &block) if defined?(::ERB) && Plezi.file_exists?(filename)
|
176
|
+
return false
|
177
|
+
end
|
178
|
+
|
179
|
+
# returns the initial method called (or about to be called) by the router for the HTTP request.
|
180
|
+
#
|
181
|
+
# this is can be very useful within the before / after filters:
|
182
|
+
# def before
|
183
|
+
# return false unless "check credentials" && [:save, :update, :delete].include?(requested_method)
|
184
|
+
#
|
185
|
+
# if the controller responds to a WebSockets request (a controller that defines the `on_message` method),
|
186
|
+
# the value returned is invalid and will remain 'stuck' on :pre_connect
|
187
|
+
# (which is the last method called before the protocol is switched from HTTP to WebSockets).
|
188
|
+
def requested_method
|
189
|
+
# respond to websocket special case
|
190
|
+
return :pre_connect if request['upgrade'] && request['upgrade'].to_s.downcase == 'websocket' && request['connection'].to_s.downcase == 'upgrade'
|
191
|
+
# respond to save 'new' special case
|
192
|
+
return :save if request.request_method.match(/POST|PUT/) && params[:id].nil? || params[:id] == 'new'
|
193
|
+
# set DELETE method if simulated
|
194
|
+
request.request_method = 'DELETE' if params[:_method].to_s.downcase == 'delete'
|
195
|
+
# respond to special :id routing
|
196
|
+
return params[:id].to_sym if params[:id] && available_public_methods.include?(params[:id].to_sym)
|
197
|
+
#review general cases
|
198
|
+
case request.request_method
|
199
|
+
when 'GET', 'HEAD'
|
200
|
+
return :index unless params[:id]
|
201
|
+
return :show
|
202
|
+
when 'POST', 'PUT'
|
203
|
+
return :update
|
204
|
+
when 'DELETE'
|
205
|
+
return :delete
|
206
|
+
end
|
207
|
+
false
|
208
|
+
end
|
209
|
+
|
210
|
+
# lists the available methods that will be exposed to HTTP requests
|
211
|
+
def available_public_methods
|
212
|
+
# set class global to improve performance while checking for supported methods
|
213
|
+
@@___available_public_methods___ ||= available_routing_methods - [:before, :after, :save, :show, :update, :delete, :initialize, :on_message, :pre_connect, :on_connect, :on_disconnect]
|
214
|
+
end
|
215
|
+
|
216
|
+
# lists the available methods that will be exposed to the HTTP router
|
217
|
+
def available_routing_methods
|
218
|
+
# set class global to improve performance while checking for supported methods
|
219
|
+
@@___available_routing_methods___ ||= (((self.class.public_instance_methods - Object.public_instance_methods) - Plezi::ControllerMagic::InstanceMethods.instance_methods).delete_if {|m| m.to_s[0] == '_'})
|
220
|
+
end
|
221
|
+
|
222
|
+
## WebSockets Magic
|
223
|
+
|
224
|
+
# WebSockets.
|
225
|
+
#
|
226
|
+
# Use this to brodcast an event to all 'sibling' websockets (websockets that have been created using the same Controller class).
|
227
|
+
#
|
228
|
+
# accepts:
|
229
|
+
# method_name:: a Symbol with the method's name that should respond to the broadcast.
|
230
|
+
# *args:: any arguments that should be passed to the method (IF REDIS IS USED, LIMITATIONS APPLY).
|
231
|
+
#
|
232
|
+
# the method will be called asynchrnously for each sibling instance of this Controller class.
|
233
|
+
#
|
234
|
+
def broadcast method_name, *args, &block
|
235
|
+
return false unless self.class.public_instance_methods.include?(method_name)
|
236
|
+
@uuid ||= SecureRandom.uuid
|
237
|
+
self.class.__inner_redis_broadcast(uuid, method_name, args, &block) || self.class.__inner_process_broadcast(uuid, method_name.to_sym, args, &block)
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
# WebSockets.
|
242
|
+
#
|
243
|
+
# Use this to collect data from all 'sibling' websockets (websockets that have been created using the same Controller class).
|
244
|
+
#
|
245
|
+
# This method will call the requested method on all instance siblings and return an Array of the returned values (including nil values).
|
246
|
+
#
|
247
|
+
# This method will block the excecution unless a block is passed to the method - in which case
|
248
|
+
# the block will used as a callback and recieve the Array as a parameter.
|
249
|
+
#
|
250
|
+
# i.e.
|
251
|
+
#
|
252
|
+
# this will block: `collect :_get_id`
|
253
|
+
#
|
254
|
+
# this will not block: `collect(:_get_id) {|a| puts "got #{a.length} responses."; a.each { |id| puts "#{id}"} }
|
255
|
+
#
|
256
|
+
# accepts:
|
257
|
+
# method_name:: a Symbol with the method's name that should respond to the broadcast.
|
258
|
+
# *args:: any arguments that should be passed to the method.
|
259
|
+
# &block:: an optional block to be used as a callback.
|
260
|
+
#
|
261
|
+
# the method will be called asynchrnously for each sibling instance of this Controller class.
|
262
|
+
def collect method_name, *args, &block
|
263
|
+
return Plezi.callback(self, :collect, *args, &block) if block
|
264
|
+
|
265
|
+
r = []
|
266
|
+
ObjectSpace.each_object(self.class) { |controller| r << controller.method(method_name).call(*args) if controller.accepts_broadcast? && (controller.object_id != self.object_id) }
|
267
|
+
return r
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
# # will (probably NOT), in the future, require authentication or, alternatively, return an Array [user_name, password]
|
272
|
+
# #
|
273
|
+
# #
|
274
|
+
# def request_http_auth realm = false, auth = 'Digest'
|
275
|
+
# return request.service.handler.hosts[request[:host] || :default].send_by_code request, 401, "WWW-Authenticate" => "#{auth}#{realm ? "realm=\"#{realm}\"" : ''}" unless request['authorization']
|
276
|
+
# request['authorization']
|
277
|
+
# end
|
278
|
+
end
|
279
|
+
|
280
|
+
module ClassMethods
|
281
|
+
public
|
282
|
+
|
283
|
+
# reviews the Redis connection, sets it up if it's missing and returns the Redis connection.
|
284
|
+
#
|
285
|
+
# todo: review thread status? (incase an exception killed it)
|
286
|
+
def redis_connection
|
287
|
+
return false unless defined?(Redis) && ENV['PL_REDIS_URL']
|
288
|
+
return @@redis if defined?(@@redis_sub_thread) && @@redis
|
289
|
+
@@redis_uri ||= URI.parse(ENV['PL_REDIS_URL'])
|
290
|
+
@@redis ||= Redis.new(host: @@redis_uri.host, port: @@redis_uri.port, password: @@redis_uri.password)
|
291
|
+
@@redis_sub_thread = Thread.new do
|
292
|
+
begin
|
293
|
+
Redis.new(host: @@redis_uri.host, port: @@redis_uri.port, password: @@redis_uri.password).subscribe(redis_channel_name) do |on|
|
294
|
+
on.message do |channel, msg|
|
295
|
+
args = JSON.parse(msg)
|
296
|
+
params = args.shift
|
297
|
+
__inner_process_broadcast params['_an_ignore_object'], params['_an_method_broadcasted'].to_sym, args
|
298
|
+
end
|
299
|
+
end
|
300
|
+
rescue Exception => e
|
301
|
+
Plezi.error e
|
302
|
+
retry
|
303
|
+
end
|
304
|
+
end
|
305
|
+
raise "Redis connction failed for: #{ENV['PL_REDIS_URL']}" unless @@redis
|
306
|
+
@@redis
|
307
|
+
end
|
308
|
+
|
309
|
+
# returns a Redis channel name for this controller.
|
310
|
+
def redis_channel_name
|
311
|
+
self.name.to_s
|
312
|
+
end
|
313
|
+
|
314
|
+
# broadcasts messages (methods) for this process
|
315
|
+
def __inner_process_broadcast ignore, method_name, args, &block
|
316
|
+
ObjectSpace.each_object(self) { |controller| Plezi.callback controller, method_name, *args, &block if controller.accepts_broadcast? && (!ignore || controller.uuid != ignore) }
|
317
|
+
end
|
318
|
+
|
319
|
+
# broadcasts messages (methods) between all processes (using Redis).
|
320
|
+
def __inner_redis_broadcast ignore, method_name, args, &block
|
321
|
+
return false unless redis_connection
|
322
|
+
raise "Radis broadcasts cannot accept blocks (no inter-process callbacks of memory sharing)!" if block
|
323
|
+
# raise "Radis broadcasts accept only one paramater, which is an optional Hash (no inter-process memory sharing)" if args.length > 1 || (args[0] && !args[0].is_a?(Hash))
|
324
|
+
args.unshift ({_an_method_broadcasted: method_name, _an_ignore_object: ignore})
|
325
|
+
redis_connection.publish(redis_channel_name, args.to_json )
|
326
|
+
true
|
327
|
+
end
|
328
|
+
|
329
|
+
# WebSockets.
|
330
|
+
#
|
331
|
+
# Class method.
|
332
|
+
#
|
333
|
+
# Use this to brodcast an event to all connections.
|
334
|
+
#
|
335
|
+
# accepts:
|
336
|
+
# method_name:: a Symbol with the method's name that should respond to the broadcast.
|
337
|
+
# *args:: any arguments that should be passed to the method (IF REDIS IS USED, LIMITATIONS APPLY).
|
338
|
+
#
|
339
|
+
# this method accepts and optional block (NON-REDIS ONLY) to be used as a callback for each sibling's event.
|
340
|
+
#
|
341
|
+
# the method will be called asynchrnously for each sibling instance of this Controller class.
|
342
|
+
def broadcast method_name, *args, &block
|
343
|
+
return false unless public_instance_methods.include?(method_name)
|
344
|
+
__inner_redis_broadcast(nil, method_name, args, &block) || __inner_process_broadcast(nil, method_name.to_sym, args, &block)
|
345
|
+
end
|
346
|
+
|
347
|
+
# WebSockets.
|
348
|
+
#
|
349
|
+
# Class method.
|
350
|
+
#
|
351
|
+
# Use this to collect data from all websockets for the calling class (websockets that have been created using the same Controller class).
|
352
|
+
#
|
353
|
+
# This method will call the requested method on all instance and return an Array of the returned values (including nil values).
|
354
|
+
#
|
355
|
+
# This method will block the excecution unless a block is passed to the method - in which case
|
356
|
+
# the block will used as a callback and recieve the Array as a parameter.
|
357
|
+
#
|
358
|
+
# i.e.
|
359
|
+
#
|
360
|
+
# this will block: `collect :_get_id`
|
361
|
+
#
|
362
|
+
# this will not block: `collect(:_get_id) {|a| puts "got #{a.length} responses."; a.each { |id| puts "#{id}"} }
|
363
|
+
#
|
364
|
+
# accepts:
|
365
|
+
# method_name:: a Symbol with the method's name that should respond to the broadcast.
|
366
|
+
# *args:: any arguments that should be passed to the method.
|
367
|
+
# &block:: an optional block to be used as a callback.
|
368
|
+
#
|
369
|
+
# the method will be called asynchrnously for each instance of this Controller class.
|
370
|
+
def collect method_name, *args, &block
|
371
|
+
return Plezi.push_event(self.method(:collect), *args, &block) if block
|
372
|
+
|
373
|
+
r = []
|
374
|
+
ObjectSpace.each_object(self) { |controller| r << controller.method(method_name).call(*args) if controller.accepts_broadcast? }
|
375
|
+
return r
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
module_function
|
380
|
+
|
381
|
+
|
382
|
+
end
|
383
|
+
end
|