plezi 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|