├── .DS_Store
├── vendor
└── cache
│ ├── rack-1.3.2.gem
│ ├── rake-0.9.2.gem
│ ├── tilt-1.3.3.gem
│ ├── watchr-0.7.gem
│ ├── execjs-1.1.2.gem
│ ├── sinatra-1.2.6.gem
│ ├── sprockets-1.0.2.gem
│ ├── uglifier-0.5.4.gem
│ ├── jslintrb_v8-1.0.1.gem
│ ├── multi_json-1.0.3.gem
│ └── therubyracer-0.8.0.gem
├── packaging
├── start_ec2.sh
├── chef-cookbook
│ └── cookbooks
│ │ └── chloe
│ │ ├── README.rdoc
│ │ ├── metadata.rb
│ │ ├── recipes
│ │ └── default.rb
│ │ └── metadata.json
├── common.sh
├── build_ubuntu.sh
└── sample_user_data.sh
├── apps
└── chloe
│ ├── src
│ ├── chloe.hrl
│ ├── chloe_yaws_websocket.erl
│ ├── chloe.app.src
│ ├── chloe_app.erl
│ ├── lib_md5.erl
│ ├── chloe.erl
│ ├── chloe_websocket_sup.erl
│ ├── chloe_session_sup.erl
│ ├── chloe_xhr_stream_sup.erl
│ ├── chloe_jsonp_stream_sup.erl
│ ├── chloe_yaws_send.erl
│ ├── chloe_yaws.erl
│ ├── chloe_sup.erl
│ ├── chloe_message.erl
│ ├── chloe_socketio_protocol.erl
│ ├── chloe_session_manager.erl
│ ├── chloe_xhr_stream.erl
│ ├── chloe_channel_store.erl
│ ├── chloe_yaws_xhr.erl
│ ├── chloe_jsonp_stream.erl
│ ├── chloe_yaws_jsonp.erl
│ ├── chloe_websocket.erl
│ └── chloe_session.erl
│ ├── test
│ ├── lib_md5_test.erl
│ ├── chloe_session_test.erl
│ ├── chloe_socketio_protocol_test.erl
│ └── gen_server_mock.erl
│ └── docs
│ ├── how_it_works.graph
│ └── architecture.dot
├── .gitignore
├── Gemfile
├── rebar.config
├── javascripts
├── chloe.js
├── chloe-xdomain.js
├── chloe-websocket.js
├── chloe-client.js
├── chloe-message.js
├── chloe-jsonp.js
├── chloe-xhr.js
└── json2.js
├── support
├── views
│ ├── index.erb
│ └── demo.erb
└── echo_server.rb
├── public
└── index.yaws
├── rel
├── files
│ ├── vm.args
│ ├── app.config
│ ├── erl
│ ├── nodetool
│ └── chloe
└── reltool.config
├── CHANGELOG
├── Gemfile.lock
├── LICENSE
├── provisioning_notes.txt
├── Rakefile
└── README.md
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/.DS_Store
--------------------------------------------------------------------------------
/vendor/cache/rack-1.3.2.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/rack-1.3.2.gem
--------------------------------------------------------------------------------
/vendor/cache/rake-0.9.2.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/rake-0.9.2.gem
--------------------------------------------------------------------------------
/vendor/cache/tilt-1.3.3.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/tilt-1.3.3.gem
--------------------------------------------------------------------------------
/vendor/cache/watchr-0.7.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/watchr-0.7.gem
--------------------------------------------------------------------------------
/vendor/cache/execjs-1.1.2.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/execjs-1.1.2.gem
--------------------------------------------------------------------------------
/vendor/cache/sinatra-1.2.6.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/sinatra-1.2.6.gem
--------------------------------------------------------------------------------
/vendor/cache/sprockets-1.0.2.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/sprockets-1.0.2.gem
--------------------------------------------------------------------------------
/vendor/cache/uglifier-0.5.4.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/uglifier-0.5.4.gem
--------------------------------------------------------------------------------
/vendor/cache/jslintrb_v8-1.0.1.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/jslintrb_v8-1.0.1.gem
--------------------------------------------------------------------------------
/vendor/cache/multi_json-1.0.3.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/multi_json-1.0.3.gem
--------------------------------------------------------------------------------
/vendor/cache/therubyracer-0.8.0.gem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mashion/chloe/HEAD/vendor/cache/therubyracer-0.8.0.gem
--------------------------------------------------------------------------------
/packaging/start_ec2.sh:
--------------------------------------------------------------------------------
1 | basedir=$(dirname $0)
2 |
3 | source $basedir/common.sh
4 | echo "Host is $(start_ubuntu_64_bit)"
5 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe.hrl:
--------------------------------------------------------------------------------
1 | -record(socketio_msg, {type, data}).
2 | -record(message, {data, version, type, channel, id, session_id}).
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | deps
2 | apps/chloe/ebin
3 | NOTES.markdown
4 | .eunit
5 | *.log
6 | foobar.access
7 | chloe.config
8 | rel/chloe*
9 | public/*.js
10 | pkgs
11 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source :rubygems
2 |
3 | gem 'rake'
4 | gem 'jslintrb_v8'
5 | gem 'watchr'
6 | gem 'therubyracer', '0.8.0'
7 | gem 'sprockets', '1.0.2'
8 | gem 'uglifier'
9 | gem 'sinatra'
10 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | {lib_dirs, ["deps"]}.
2 |
3 | {deps, [
4 | %% yaws-1.91
5 | {yaws, ".*", {git, "https://github.com/mashion/yaws.git", {branch, "klacke-yaws-1.91"}}}
6 | ]}.
7 |
8 | {sub_dirs, ["apps/chloe", "rel"]}.
9 |
--------------------------------------------------------------------------------
/javascripts/chloe.js:
--------------------------------------------------------------------------------
1 | //= require "json2.js"
2 | //= require "chloe-client.js"
3 | //= require "chloe-websocket.js"
4 | //= require "chloe-xhr.js"
5 | //= require "chloe-xdomain.js"
6 | //= require "chloe-jsonp.js"
7 | //= require "chloe-message.js"
8 |
--------------------------------------------------------------------------------
/apps/chloe/test/lib_md5_test.erl:
--------------------------------------------------------------------------------
1 | -module(lib_md5_test).
2 |
3 | -include_lib("eunit/include/eunit.hrl").
4 |
5 | hexdigest_test() ->
6 | "49f68a5c8493ec2c0bf489821c21fc3b" = lib_md5:hexdigest("hi"),
7 | "c58d6a0c84499d3f992fb23d1348af52" = lib_md5:hexdigest("trotter").
8 |
--------------------------------------------------------------------------------
/packaging/chef-cookbook/cookbooks/chloe/README.rdoc:
--------------------------------------------------------------------------------
1 | = DESCRIPTION:
2 |
3 | Installs / Configures chloe, a realtime webserver that doesn't suck.
4 |
5 | = REQUIREMENTS:
6 |
7 | Known to work on Debian / Ubuntu. May work on others, but is untested.
8 |
9 | = ATTRIBUTES:
10 |
11 | = USAGE:
12 |
13 |
--------------------------------------------------------------------------------
/apps/chloe/docs/how_it_works.graph:
--------------------------------------------------------------------------------
1 | [ Browser ] -- 1. /index.html --> [ Your App ]
2 | [ Browser ] -- 2. Send data over websockets --> [ Chloe ]
3 | [ Chloe ] -- 3. Data from the browser --> [ Your App ]
4 | [ Your App ] -- 4. POST /send (data for browser) --> [ Chloe ]
5 | [ Chloe ] -- 5. Data from your app --> [ Browser ]
6 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_yaws_websocket.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_yaws_websocket).
2 |
3 | -include("../../../deps/yaws/include/yaws_api.hrl").
4 |
5 | -export([out/1]).
6 |
7 | -define(ACTIVE, true).
8 |
9 | out(A) ->
10 | {ok, WebSocketOwner} = chloe_websocket_sup:start_child(),
11 | yaws_websockets:handshake(A, WebSocketOwner, true).
12 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe.app.src:
--------------------------------------------------------------------------------
1 | {application, chloe,
2 | [
3 | {description, "A websocket server"},
4 | {vsn, "0.0.5"},
5 | {registered, [chloe_app]},
6 | {applications, [
7 | kernel,
8 | stdlib,
9 | inets
10 | ]},
11 | {mod, { chloe_app, []}},
12 | {env, []}
13 | ]}.
14 |
--------------------------------------------------------------------------------
/support/views/index.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Chloe
5 |
6 |
7 | Hello, I am Chloe.
8 | Look at my source to see how to use chloe.
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/javascripts/chloe-xdomain.js:
--------------------------------------------------------------------------------
1 | Chloe.XDomainTransport = function (options) {
2 | Chloe.XhrTransport.apply(this, [options]);
3 | };
4 |
5 | Chloe.XDomainTransport.isEnabled = function () {
6 | return 'XDomainRequest' in window;
7 | };
8 |
9 | Chloe.XDomainTransport.prototype = Chloe.extend(Chloe.XhrTransport.prototype, {
10 | makeXhr: function () {
11 | return new XDomainRequest();
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/packaging/chef-cookbook/cookbooks/chloe/metadata.rb:
--------------------------------------------------------------------------------
1 | maintainer "Trotter Cashion"
2 | maintainer_email "cashion@gmail.com"
3 | license "MIT"
4 | description "Installs/Configures chloe"
5 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.rdoc'))
6 | version "0.0.1"
7 | depends "erlang"
8 | depends "git"
9 | supports "ubuntu"
10 | supports "debian"
11 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_app.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_app).
2 |
3 | -behaviour(application).
4 |
5 | %% Application callbacks
6 | -export([start/2, stop/1]).
7 |
8 | %% ===================================================================
9 | %% Application callbacks
10 | %% ===================================================================
11 |
12 | start(_StartType, _StartArgs) ->
13 | chloe_sup:start_link().
14 |
15 | stop(_State) ->
16 | ok.
17 |
--------------------------------------------------------------------------------
/public/index.yaws:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Chloe
5 |
6 |
7 | Hello, I am Chloe.
8 | Look at my source to see how to use chloe.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/apps/chloe/src/lib_md5.erl:
--------------------------------------------------------------------------------
1 | -module(lib_md5).
2 |
3 | -export([
4 | hexdigest/1
5 | ]).
6 |
7 | %%--------------------------------------------------------------------
8 | %% API functions
9 | %%--------------------------------------------------------------------
10 |
11 | hexdigest(String) ->
12 | Accumulator = fun (Byte, Digest) ->
13 | Digest ++ lists:flatten(io_lib:format("~2.16.0b", [Byte]))
14 | end,
15 | lists:foldl(Accumulator, "", binary_to_list(erlang:md5(String))).
16 |
--------------------------------------------------------------------------------
/rel/files/vm.args:
--------------------------------------------------------------------------------
1 |
2 | ## Name of the node
3 | -name chloe@127.0.0.2
4 |
5 | ## Cookie for distributed erlang
6 | -setcookie chloe
7 |
8 | ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive
9 | ## (Disabled by default..use with caution!)
10 | ##-heart
11 |
12 | ## Enable kernel poll and a few async threads
13 | +K true
14 | +A 5
15 |
16 | ## Increase number of concurrent ports/sockets
17 | -env ERL_MAX_PORTS 4096
18 |
19 | ## Tweak GC to run more often
20 | -env ERL_FULLSWEEP_AFTER 10
21 |
22 |
--------------------------------------------------------------------------------
/support/views/demo.erb:
--------------------------------------------------------------------------------
1 | var chloe = new Chloe({host: "<%= server_name %>", port: 8901});
2 |
3 | chloe.onmessage(function (message) {
4 | alert('I got a message too: ' + message);
5 | });
6 |
7 | chloe.onclose(function () {
8 | alert("And... we're closed.");
9 | });
10 |
11 | chloe.connect(function () {
12 | alert('Holy crap, connected!');
13 | chloe.send('Ohai Chloe!');
14 |
15 | chloe.subscribe('pumpkin', function (message) {
16 | alert('Someone was eating pumpkins: ' + message);
17 | });
18 | });
19 |
20 |
--------------------------------------------------------------------------------
/packaging/common.sh:
--------------------------------------------------------------------------------
1 | function start_instance() {
2 | instance_id=$(ec2-run-instances $@ | grep INSTANCE | cut -f2)
3 | host=""
4 |
5 | while [ -z "$host" ]
6 | do
7 | host=$(ec2-describe-instances | grep $instance_id | cut -f4)
8 | sleep 1
9 | done
10 |
11 | echo "$host"
12 | }
13 |
14 | function start_ubuntu_32_bit() {
15 | start_instance ami-a6f504cf -k trotter-personal-ec2
16 | }
17 |
18 | function start_ubuntu_64_bit() {
19 | start_instance ami-08f40561 -t m1.large -k trotter-personal-ec2
20 | }
21 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | == HEAD
2 |
3 | - Sign requests coming from the Chloe server
4 |
5 | == 0.0.5
6 |
7 | - Fix sendfile issue with Ubuntu
8 |
9 | == 0.0.4
10 |
11 | - Fixed a memory leak
12 | - Allowed transports is now configurable
13 | - Optionally require messages from app server to be signed
14 | - Discontinue XHR support
15 | - Discontinue Chrome WebSockets (will be added back soon)
16 | - Support Safari WebSockets
17 |
18 | == 0.0.3
19 |
20 | - Deployment fixes
21 | - New YAWS version
22 | - Fixed file streaming on ubuntu
23 |
24 | == 0.0.2
25 |
26 | - Xhr Support
27 | - IE fixes
28 |
--------------------------------------------------------------------------------
/apps/chloe/docs/architecture.dot:
--------------------------------------------------------------------------------
1 | graph architecture {
2 | chloe_yaws_updates -- websocket;
3 | chloe_yaws_updates -- longpoll;
4 | chloe_yaws_updates -- flash;
5 | chloe_yaws_updates -- others;
6 |
7 | subgraph cluster_transports {
8 | label = "transports";
9 | websocket;
10 | longpoll;
11 | flash;
12 | others;
13 | }
14 |
15 | websocket -- session;
16 | longpoll -- session;
17 | flash -- session;
18 | others -- session;
19 |
20 | chloe_yaws_send -- session;
21 | session -- channel;
22 |
23 | session -- socket_io_parser;
24 |
25 | chloe_yaws_send -- channel;
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/apps/chloe/test/chloe_session_test.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_session_test).
2 |
3 | -include_lib("eunit/include/eunit.hrl").
4 | -include_lib("../src/chloe.hrl").
5 |
6 | send_to_browser_test() ->
7 | {ok, MockPid} = gen_server_mock:new(),
8 | gen_server_mock:expect_cast(MockPid, fun({send, [[Message]]}, State) ->
9 | "/all" = Message#message.channel,
10 | "some test data" = Message#message.data,
11 | {ok, State}
12 | end),
13 |
14 | {ok, SessionPid} = chloe_session:start_link(MockPid),
15 | chloe_session:send_to_browser(SessionPid, "/all", "some test data"),
16 | gen_server_mock:assert_expectations(MockPid).
17 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: http://rubygems.org/
3 | specs:
4 | execjs (1.1.2)
5 | multi_json (~> 1.0)
6 | jslintrb_v8 (1.0.1)
7 | therubyracer (= 0.8.0)
8 | multi_json (1.0.3)
9 | rack (1.3.2)
10 | rake (0.9.2)
11 | sinatra (1.2.6)
12 | rack (~> 1.1)
13 | tilt (< 2.0, >= 1.2.2)
14 | sprockets (1.0.2)
15 | therubyracer (0.8.0)
16 | tilt (1.3.3)
17 | uglifier (0.5.4)
18 | execjs (>= 0.3.0)
19 | multi_json (>= 1.0.2)
20 | watchr (0.7)
21 |
22 | PLATFORMS
23 | ruby
24 |
25 | DEPENDENCIES
26 | jslintrb_v8
27 | rake
28 | sinatra
29 | sprockets (= 1.0.2)
30 | therubyracer (= 0.8.0)
31 | uglifier
32 | watchr
33 |
--------------------------------------------------------------------------------
/rel/files/app.config:
--------------------------------------------------------------------------------
1 | [
2 | %% SASL config
3 | {sasl, [
4 | {sasl_error_logger, {file, "log/sasl-error.log"}},
5 | {errlog_type, error},
6 | {error_logger_mf_dir, "log/sasl"}, % Log directory
7 | {error_logger_mf_maxbytes, 10485760}, % 10 MB max file size
8 | {error_logger_mf_maxfiles, 5} % 5 files max
9 | ]},
10 |
11 | %% Chloe config
12 | {chloe,
13 | [
14 | {application_server, "http://localhost:4567"},
15 | {application_server_url, "http://localhost:4567/updates"},
16 | {port, 8901},
17 | {doc_root, "./public"},
18 | %% {secret, "YOUR_SECRET_GOES_HERE"},
19 | {log_dir, "/var/log/chloe"}
20 | ]}
21 | ].
22 |
23 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe.erl:
--------------------------------------------------------------------------------
1 | -module(chloe).
2 |
3 | %% API
4 | -export([
5 | start/0,
6 | stop/0
7 | ]).
8 |
9 | %%--------------------------------------------------------------------
10 | %% API
11 | %%--------------------------------------------------------------------
12 |
13 | start() ->
14 | ensure_started(inets),
15 | application:start(chloe).
16 |
17 | stop() ->
18 | application:stop(chloe).
19 |
20 | %%--------------------------------------------------------------------
21 | %% Internal functions
22 | %%--------------------------------------------------------------------
23 |
24 | ensure_started(App) ->
25 | case application:start(App) of
26 | ok ->
27 | ok;
28 | {error, {already_started, App}} ->
29 | ok
30 | end.
31 |
--------------------------------------------------------------------------------
/packaging/chef-cookbook/cookbooks/chloe/recipes/default.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Cookbook Name:: chloe
3 | # Recipe:: default
4 | #
5 | # Copyright 2011, Mashion, LLC
6 | #
7 | # MIT Licensed
8 | #
9 |
10 | directory "/opt"
11 |
12 | gem_package "sinatra"
13 |
14 | execute "untar chloe" do
15 | command "cd /opt && tar xzvf /tmp/chloe.tgz"
16 | end
17 |
18 | remote_file "/tmp/chloe.tgz" do
19 | source "https://s3.amazonaws.com/chloe-trotter/chloe-0.0.1-ubuntu32.tgz"
20 | mode "0644"
21 | notifies :run, resource(:execute => "untar chloe")
22 | end
23 |
24 | remote_file "/tmp/chloe_chat_example.tgz" do
25 | source "https://s3.amazonaws.com/chloe-trotter/chloe_chat_example.tgz"
26 | mode "0644"
27 | end
28 |
29 | execute "untar echo server" do
30 | command "cd /opt && tar xzvf /tmp/chloe_chat_example.tgz"
31 | not_if { File.exist?("/opt/echo_server.tgz") }
32 | end
33 |
--------------------------------------------------------------------------------
/apps/chloe/test/chloe_socketio_protocol_test.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_socketio_protocol_test).
2 |
3 | -include_lib("eunit/include/eunit.hrl").
4 | -include_lib("../src/chloe.hrl").
5 |
6 | parse_message_with_empty_realm_test() ->
7 | {ok, Msg} = chloe_socketio_protocol:parse(<<"1:6::hello,">>),
8 | message = Msg#socketio_msg.type,
9 | <<"hello">> = Msg#socketio_msg.data.
10 |
11 | parse_message_with_javascript_payload_test() ->
12 | Payload = "1:32:j\n:{\"name\":\"yup\",\"message\":\"hi\"},",
13 | {ok, Msg} = chloe_socketio_protocol:parse(list_to_binary(Payload)),
14 | message = Msg#socketio_msg.type,
15 | <<"{\"name\":\"yup\",\"message\":\"hi\"}">> = Msg#socketio_msg.data.
16 |
17 | pack_message_test() ->
18 | "1:6::hello," = chloe_socketio_protocol:pack(message, "", "hello").
19 |
20 | pack_handshake_test() ->
21 | "3:3:256," = chloe_socketio_protocol:pack(handshake, "256").
22 |
--------------------------------------------------------------------------------
/packaging/chef-cookbook/cookbooks/chloe/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chloe",
3 | "description": "Installs/Configures chloe",
4 | "long_description": "= DESCRIPTION:\n\nInstalls / Configures chloe, a realtime webserver that doesn't suck.\n\n= REQUIREMENTS:\n\nKnown to work on Debian / Ubuntu. May work on others, but is untested.\n\n= ATTRIBUTES:\n\n= USAGE:\n\n",
5 | "maintainer": "Trotter Cashion",
6 | "maintainer_email": "cashion@gmail.com",
7 | "license": "MIT",
8 | "platforms": {
9 | "ubuntu": [
10 |
11 | ],
12 | "debian": [
13 |
14 | ]
15 | },
16 | "dependencies": {
17 | "erlang": [
18 |
19 | ],
20 | "git": [
21 |
22 | ]
23 | },
24 | "recommendations": {
25 | },
26 | "suggestions": {
27 | },
28 | "conflicting": {
29 | },
30 | "providing": {
31 | },
32 | "replacing": {
33 | },
34 | "attributes": {
35 | },
36 | "groupings": {
37 | },
38 | "recipes": {
39 | },
40 | "version": "0.0.1"
41 | }
--------------------------------------------------------------------------------
/rel/reltool.config:
--------------------------------------------------------------------------------
1 | {sys, [
2 | {lib_dirs, ["../apps", "../deps"]},
3 | {rel, "chloe", "0.0.5",
4 | [
5 | kernel,
6 | stdlib,
7 | sasl,
8 | inets,
9 | chloe
10 | ]},
11 | {rel, "start_clean", "",
12 | [
13 | kernel,
14 | stdlib,
15 | inets
16 | ]},
17 | {boot_rel, "chloe"},
18 | {profile, embedded},
19 | {excl_sys_filters, ["^bin/.*",
20 | "^erts.*/bin/(dialyzer|typer)"]},
21 | {app, chloe, [{incl_cond, include}]}
22 | ]}.
23 |
24 | {overlay, [
25 | {mkdir, "log/sasl"},
26 | {copy, "files/erl", "{{erts_vsn}}/bin/erl"},
27 | {copy, "files/nodetool", "{{erts_vsn}}/bin/nodetool"},
28 | {copy, "files/chloe", "bin/chloe"},
29 | {copy, "files/app.config", "etc/app.config"},
30 | {copy, "files/vm.args", "etc/vm.args"},
31 | {copy, "../public", "public"}
32 | ]}.
33 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_websocket_sup.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_websocket_sup).
2 |
3 | -behaviour(supervisor).
4 |
5 | %% API
6 | -export([
7 | start_link/0,
8 | start_child/0
9 | ]).
10 |
11 | %% Supervisor callbacks
12 | -export([init/1]).
13 |
14 | -define(SERVER, ?MODULE).
15 |
16 | %%--------------------------------------------------------------------
17 | %% API functions
18 | %%--------------------------------------------------------------------
19 |
20 | start_link() ->
21 | supervisor:start_link({local, ?SERVER}, ?MODULE, []).
22 |
23 | start_child() ->
24 | supervisor:start_child(?SERVER, []).
25 |
26 | %%--------------------------------------------------------------------
27 | %% Supervisor callbacks
28 | %%--------------------------------------------------------------------
29 |
30 | init([]) ->
31 | Websocket = {chloe_websocket, {chloe_websocket, start_link, []},
32 | temporary, brutal_kill, worker, [chloe_websocket]},
33 | Children = [Websocket],
34 | RestartStrategy = {simple_one_for_one, 0, 1},
35 | {ok, {RestartStrategy, Children}}.
36 |
--------------------------------------------------------------------------------
/packaging/build_ubuntu.sh:
--------------------------------------------------------------------------------
1 | basedir=$(dirname $0)
2 | source $basedir/common.sh
3 |
4 | host="${1:-"ubuntu@$(start_ubuntu_32_bit)"}"
5 |
6 | ssh $host <
21 | supervisor:start_link({local, ?SERVER}, ?MODULE, []).
22 |
23 | start_child(TransportPid) ->
24 | supervisor:start_child(?SERVER, [TransportPid]).
25 |
26 | %%--------------------------------------------------------------------
27 | %% Supervisor callbacks
28 | %%--------------------------------------------------------------------
29 |
30 | init([]) ->
31 | Session = {chloe_session, {chloe_session, start_link, []},
32 | temporary, brutal_kill, worker, [chloe_session]},
33 | Children = [Session],
34 | RestartStrategy = {simple_one_for_one, 0, 1},
35 | {ok, {RestartStrategy, Children}}.
36 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_xhr_stream_sup.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_xhr_stream_sup).
2 |
3 | -behaviour(supervisor).
4 |
5 | %% API
6 | -export([
7 | start_link/0,
8 | start_child/2
9 | ]).
10 |
11 | %% Supervisor callbacks
12 | -export([init/1]).
13 |
14 | -define(SERVER, ?MODULE).
15 |
16 | %%--------------------------------------------------------------------
17 | %% API functions
18 | %%--------------------------------------------------------------------
19 |
20 | start_link() ->
21 | supervisor:start_link({local, ?SERVER}, ?MODULE, []).
22 |
23 | start_child(Socket, Message) ->
24 | supervisor:start_child(?SERVER, [Socket, Message]).
25 |
26 | %%--------------------------------------------------------------------
27 | %% Supervisor callbacks
28 | %%--------------------------------------------------------------------
29 |
30 | init([]) ->
31 | Stream = {chloe_xhr_stream, {chloe_xhr_stream, start_link, []},
32 | temporary, brutal_kill, worker, [chloe_session]},
33 | Children = [Stream],
34 | RestartStrategy = {simple_one_for_one, 0, 1},
35 | {ok, {RestartStrategy, Children}}.
36 |
--------------------------------------------------------------------------------
/support/echo_server.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'sinatra'
3 | require 'net/http'
4 | require 'uri'
5 | require 'digest/md5'
6 |
7 | # SECRET = "YOUR_SECRET_GOES_HERE"
8 |
9 | set :public, File.dirname(__FILE__) + '/public'
10 | set :views, File.dirname(__FILE__) + '/views'
11 |
12 | get '/' do
13 | erb :index
14 | end
15 |
16 | get '/demo.js' do
17 | content_type :js
18 | erb :demo
19 | end
20 |
21 | post '/updates' do
22 | puts request.inspect
23 | signature = request.env["HTTP_X_CHLOE_SIGNATURE"]
24 | raw_data = request.body.read
25 | data = "Handled by Sinatra: #{raw_data}"
26 |
27 | if SECRET && signature != Digest::MD5.hexdigest(raw_data + SECRET)
28 | puts "Signature invalid: signature=#{signature};calculated=#{Digest::MD5.hexdigest(raw_data + SECRET)}"
29 | return "failure"
30 | end
31 |
32 | sig = Digest::MD5.hexdigest(data + SECRET)
33 | Net::HTTP.post_form(URI.parse("http://#{server_name}:8901/send"),
34 | {"data" => data, "sig" => sig})
35 | puts "I got some data: #{data}"
36 | "success"
37 | end
38 |
39 | def server_name
40 | @request.env["SERVER_NAME"]
41 | end
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2011 Trotter Cashion
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_jsonp_stream_sup.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_jsonp_stream_sup).
2 |
3 | -behaviour(supervisor).
4 |
5 | %% API
6 | -export([
7 | start_link/0,
8 | start_child/2
9 | ]).
10 |
11 | %% Supervisor callbacks
12 | -export([init/1]).
13 |
14 | -define(SERVER, ?MODULE).
15 |
16 | %%--------------------------------------------------------------------
17 | %% API functions
18 | %%--------------------------------------------------------------------
19 |
20 | start_link() ->
21 | supervisor:start_link({local, ?SERVER}, ?MODULE, []).
22 |
23 | start_child(Socket, Message) ->
24 | supervisor:start_child(?SERVER, [Socket, Message]).
25 |
26 | %%--------------------------------------------------------------------
27 | %% Supervisor callbacks
28 | %%--------------------------------------------------------------------
29 |
30 | init([]) ->
31 | JsonpStream = {chloe_jsonp_stream, {chloe_jsonp_stream, start_link, []},
32 | temporary, brutal_kill, worker, [chloe_session]},
33 | Children = [JsonpStream],
34 | RestartStrategy = {simple_one_for_one, 0, 1},
35 | {ok, {RestartStrategy, Children}}.
36 |
--------------------------------------------------------------------------------
/packaging/sample_user_data.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | log="/tmp/init.log"
3 |
4 | apt-get update >> $log
5 | apt-get install -y ruby irb ri libopenssl-ruby1.8 libshadow-ruby1.8 ruby1.8-dev gcc g++ rsync curl >> $log
6 |
7 | curl -L 'http://production.cf.rubygems.org/rubygems/rubygems-1.6.2.tgz' | tar xvzf -
8 | cd rubygems* && ruby setup.rb --no-ri --no-rdoc >> $log
9 | ln -sfv /usr/bin/gem1.8 /usr/bin/gem
10 |
11 | gem install rdoc chef ohai --no-ri --no-rdoc --source http://gems.opscode.com --source http://gems.rubyforge.org >> $log
12 |
13 | mkdir -p /etc/chef
14 |
15 | cat << EOF > /etc/chef/solo.rb
16 | file_cache_path "/var/chef/cache"
17 | file_backup_path "/var/chef/backup"
18 | cookbook_path ["/var/chef/cookbooks"]
19 | role_path "/var/chef/roles"
20 | json_attribs "/etc/chef/node.json"
21 | verbose_logging true
22 | EOF
23 |
24 | cat << EOF > /etc/chef/node.json
25 | {
26 | "run_list": ["recipe[chloe]"]
27 | }
28 | EOF
29 |
30 | chef-solo -r https://s3.amazonaws.com/chloe-trotter/chloe-chef-solo.tgz >> $log 2>&1
31 |
32 | while ! ls /opt/chloe_chat_example
33 | do
34 | sleep 1
35 | done
36 |
37 | cd /opt/chloe-0.0.5/bin && ./chloe start
38 | /usr/bin/ruby /opt/chloe_chat_example/chat_server.rb > /var/chat_server.log 2>&1 &
39 |
--------------------------------------------------------------------------------
/rel/files/erl:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ## This script replaces the default "erl" in erts-VSN/bin. This is necessary
4 | ## as escript depends on erl and in turn, erl depends on having access to a
5 | ## bootscript (start.boot). Note that this script is ONLY invoked as a side-effect
6 | ## of running escript -- the embedded node bypasses erl and uses erlexec directly
7 | ## (as it should).
8 | ##
9 | ## Note that this script makes the assumption that there is a start_clean.boot
10 | ## file available in $ROOTDIR/release/VSN.
11 |
12 | # Determine the abspath of where this script is executing from.
13 | ERTS_BIN_DIR=$(cd ${0%/*} && pwd)
14 |
15 | # Now determine the root directory -- this script runs from erts-VSN/bin,
16 | # so we simply need to strip off two dirs from the end of the ERTS_BIN_DIR
17 | # path.
18 | ROOTDIR=${ERTS_BIN_DIR%/*/*}
19 |
20 | # Parse out release and erts info
21 | START_ERL=`cat $ROOTDIR/releases/start_erl.data`
22 | ERTS_VSN=${START_ERL% *}
23 | APP_VSN=${START_ERL#* }
24 |
25 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin
26 | EMU=beam
27 | PROGNAME=`echo $0 | sed 's/.*\\///'`
28 | CMD="$BINDIR/erlexec"
29 | export EMU
30 | export ROOTDIR
31 | export BINDIR
32 | export PROGNAME
33 |
34 | exec $CMD -boot $ROOTDIR/releases/$APP_VSN/start_clean ${1+"$@"}
--------------------------------------------------------------------------------
/javascripts/chloe-websocket.js:
--------------------------------------------------------------------------------
1 | Chloe.WebSocketTransport = function (options) {
2 | this.host = options.host;
3 | this.port = options.port;
4 | this.socketAttributes = {};
5 | };
6 |
7 | Chloe.WebSocketTransport.prototype = {
8 | // Public API
9 | connect: function (callback) {
10 | var self = this;
11 | this.socket = new WebSocket("ws://" + this.host + ":" + this.port + "/chloe/websocket");
12 | this.socket.onopen = callback;
13 | for (var i in this.socketAttributes) {
14 | this.socket[i] = this.socketAttributes[i];
15 | }
16 | },
17 | onclose: function (callback) {
18 | this.attachToSocket('onclose', callback);
19 | },
20 | onmessage: function (callback) {
21 | this.attachToSocket('onmessage', function (message) {
22 | callback(message.data);
23 | });
24 | },
25 | send: function (message) {
26 | this.socket.send(message);
27 | },
28 |
29 | // Internal helpers
30 | attachToSocket: function (attribute, callback) {
31 | this.socketAttributes[attribute] = callback;
32 | if (this.socket) {
33 | this.socket[attribute] = this.socketAttributes[attribute];
34 | }
35 | }
36 | };
37 |
38 | Chloe.WebSocketTransport.isEnabled = function () {
39 | return typeof(WebSocket) === "object";
40 | };
41 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_yaws_send.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_yaws_send).
2 |
3 | -include("../../../deps/yaws/include/yaws_api.hrl").
4 |
5 | -export([out/1]).
6 |
7 | out(A) ->
8 | case is_message_authenticated(A) of
9 | true -> send_to_all_subscribers(A),
10 | {content, "text/plain", "success"};
11 | _ -> {content, "text/plain", "unauthenticated"}
12 | end.
13 |
14 | %%--------------------------------------------------------------------
15 | %% internal functions
16 | %%--------------------------------------------------------------------
17 |
18 | is_message_authenticated(A) ->
19 | case application:get_env(chloe, secret) of
20 | undefined -> true;
21 | {ok, Secret} -> check_signature(A, Secret)
22 | end.
23 |
24 | check_signature(A, Secret) ->
25 | {ok, Data} = yaws_api:postvar(A, "data"),
26 | {ok, Sig} = yaws_api:postvar(A, "sig"),
27 | lib_md5:hexdigest(Data ++ Secret) =:= Sig.
28 |
29 | send_to_all_subscribers(A) ->
30 | Channel = case yaws_api:postvar(A, "channel") of
31 | {ok, C} -> C;
32 | _ -> "/all"
33 | end,
34 | {ok, Subscribers} = chloe_channel_store:fetch_subscribers(Channel),
35 | {ok, Data} = yaws_api:postvar(A, "data"),
36 | lists:foreach(
37 | fun(Pid) ->
38 | error_logger:info_msg("Sending ~p to ~p~n", [Data, Channel]),
39 | chloe_session:send_to_browser(Pid, Channel, Data)
40 | end,
41 | Subscribers).
42 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_yaws.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_yaws).
2 |
3 | %% API
4 | -export([
5 | start_link/0,
6 | run/0
7 | ]).
8 |
9 | %% Supervisor module
10 | -define(SUPERVISOR, chloe_sup).
11 |
12 | start_link() ->
13 | {ok, spawn(?MODULE, run, [])}.
14 |
15 | run() ->
16 | Id = "embedded",
17 | GconfList = [{id, Id},
18 | {logdir, get_env(chloe, log_dir, ".")}],
19 | Docroot = get_env(chloe, doc_root, "./public"),
20 | SconfList = [{port, get_env(chloe, port, 8901)},
21 | {servername, "chloe"},
22 | {listen, {0,0,0,0}},
23 | {docroot, Docroot},
24 | {appmods, [{"/chloe/websocket", chloe_yaws_websocket},
25 | {"/chloe/xhr", chloe_yaws_xhr},
26 | {"/chloe/jsonp.js", chloe_yaws_jsonp},
27 | {"/send", chloe_yaws_send}]}],
28 | {ok, SCList, GC, ChildSpecs} =
29 | yaws_api:embedded_start_conf(Docroot, SconfList, GconfList, Id),
30 | [supervisor:start_child(?SUPERVISOR, Ch) || Ch <- ChildSpecs],
31 | yaws_api:setconf(GC, SCList),
32 | {ok, self()}.
33 |
34 | %%--------------------------------------------------------------------
35 | %% Internal functions
36 | %%--------------------------------------------------------------------
37 | get_env(AppName, Key, Default) ->
38 | case application:get_env(AppName, Key) of
39 | undefined -> Default;
40 | {ok, Value} -> Value
41 | end.
42 |
--------------------------------------------------------------------------------
/provisioning_notes.txt:
--------------------------------------------------------------------------------
1 | Commands to get chloe package built:
2 | $ instance_id=$(ec2-run-instances ami-a6f504cf -k trotter | grep INSTANCE | cut -f2)
3 | $ host=$(ec2-describe-instances | grep $instance_id | cut -f4)
4 | $ ssh ubuntu@$host
5 | > sudo apt-get update
6 | > sudo apt-get install -y build-essential libncurses5-dev openssl libssl-dev
7 | > sudo apt-get install -y erlang # Installs R13B03, should probably install erlang / find better package
8 | > sudo apt-get install -y git-core
9 | > sudo apt-get install -y rake
10 | > sudo apt-get install -y gcc
11 | > sudo apt-get install -y libpam0g-dev
12 | > wget http://www.erlang.org/download/otp_src_R14B02.tar.gz
13 | > tar zxvf otp_src_R14B02.tar.gz
14 | > cd otp_src_R14B02
15 | > ./configure && make && sudo make install
16 | > cd -
17 | > git clone https://github.com/basho/rebar.git
18 | > cd rebar
19 | > ./bootstrap
20 | > sudo mv rebar /usr/local/bin
21 | > cd -
22 | > git clone https://github.com/mashion/chloe.git
23 | > cd chloe
24 | > rake bootstrap
25 | > rake compile
26 | > rebar generate
27 | > cd rel
28 | > mv chloe chloe-0.0.1
29 | > tar czvf chloe-0.0.1-ubuntu.tgz chloe-0.0.1
30 |
31 | Commands to run chloe package on ubuntu on ec2:
32 | $ instance_id=$(ec2-run-instances ami-a6f504cf -k trotter | grep INSTANCE | cut -f2)
33 | $ host=$(ec2-describe-instances | grep $instance_id | cut -f4)
34 | $ ssh ubuntu@$host
35 | $ wget --no-check-certificate https://github.com/downloads/mashion/chloe/chloe-0.0.1-ubuntu32.tgz
36 | $ tar xzvf chloe-0.0.1-ubuntu32.tgz
37 | $ cd chloe-0.0.1 && ./bin/chloe start
38 |
39 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_sup.erl:
--------------------------------------------------------------------------------
1 |
2 | -module(chloe_sup).
3 |
4 | -behaviour(supervisor).
5 |
6 | %% API
7 | -export([start_link/0]).
8 |
9 | %% Supervisor callbacks
10 | -export([init/1]).
11 |
12 | %% Helper macro for declaring children of supervisor
13 | -define(WORKER(I), {I, {I, start_link, []}, permanent, 5000, worker, [I]}).
14 | -define(SUPERVISOR(I, Child), {I, {I, start_link, []}, permanent, 5000, supervisor, [I, Child]}).
15 |
16 | %% ===================================================================
17 | %% API functions
18 | %% ===================================================================
19 |
20 | start_link() ->
21 | supervisor:start_link({local, ?MODULE}, ?MODULE, []).
22 |
23 | %% ===================================================================
24 | %% Supervisor callbacks
25 | %% ===================================================================
26 |
27 | init([]) ->
28 | Yaws = ?WORKER(chloe_yaws),
29 | ChannelStore = ?WORKER(chloe_channel_store),
30 | SessionManager = ?WORKER(chloe_session_manager),
31 | WebSocketSup = ?SUPERVISOR(chloe_websocket_sup, chloe_websocket),
32 | JsonpStreamSup = ?SUPERVISOR(chloe_jsonp_stream_sup, chloe_jsonp_stream),
33 | XhrStreamSup = ?SUPERVISOR(chloe_xhr_stream_sup, chloe_xhr_stream),
34 | SessionSup = ?SUPERVISOR(chloe_session_sup, chloe_session),
35 | Children = [Yaws, ChannelStore, SessionManager, WebSocketSup,
36 | JsonpStreamSup, XhrStreamSup, SessionSup],
37 | RestartStrategy = {one_for_one, 5, 10},
38 | {ok, {RestartStrategy, Children}}.
39 |
40 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_message.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_message).
2 |
3 | %% API
4 | -export([
5 | unpack/1,
6 | pack/1
7 | ]).
8 |
9 | -define(VERSION, 1).
10 | -include_lib("./chloe.hrl").
11 |
12 | %%--------------------------------------------------------------------
13 | %% API
14 | %%--------------------------------------------------------------------
15 |
16 | unpack(Data) when is_binary(Data) ->
17 | unpack(binary_to_list(Data));
18 | unpack(Data) ->
19 | {ok, {struct, PropList}} = json:decode_string(Data),
20 | check_version(PropList),
21 | #message{data=proplists:get_value(data, PropList),
22 | version=proplists:get_value(version, PropList),
23 | type=proplists:get_value(type, PropList),
24 | channel=proplists:get_value(channel, PropList),
25 | id=proplists:get_value(id, PropList),
26 | session_id=proplists:get_value(sessionId, PropList)}.
27 |
28 | to_struct(Message) ->
29 | {struct, [{data, Message#message.data},
30 | {version, ?VERSION},
31 | {type, Message#message.type},
32 | {channel, Message#message.channel},
33 | {id, Message#message.id},
34 | {sessionId, Message#message.session_id}]}.
35 |
36 | pack(Messages) when is_list(Messages) ->
37 | json:encode({struct, [{messages, {array, lists:map(fun (M) ->
38 | to_struct(M)
39 | end, Messages)}}]});
40 | pack(Message) ->
41 | json:encode(to_struct(Message)).
42 |
43 | %%--------------------------------------------------------------------
44 | %% Internal functions
45 | %%--------------------------------------------------------------------
46 |
47 | check_version(PropList) ->
48 | ?VERSION = proplists:get_value(version, PropList).
49 |
50 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_socketio_protocol.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_socketio_protocol).
2 |
3 | %% API
4 | -export([
5 | parse/1,
6 | pack/2,
7 | pack/3
8 | ]).
9 |
10 | -include_lib("./chloe.hrl").
11 |
12 | %%--------------------------------------------------------------------
13 | %% API
14 | %%--------------------------------------------------------------------
15 |
16 | parse(Data) ->
17 | parse(Data, #socketio_msg{}).
18 |
19 | pack(message, Realm, Data) when is_binary(Data) ->
20 | pack(message, Realm, binary_to_list(Data));
21 | pack(message, Realm, Data) ->
22 | Body = lists:append([Realm, ":", Data]),
23 | lists:append(["1:", integer_to_list(length(Body)), ":", Body, ","]).
24 |
25 | pack(handshake, SessionId) ->
26 | lists:append(["3:", integer_to_list(length(SessionId)), ":", SessionId, ","]).
27 |
28 | %%--------------------------------------------------------------------
29 | %% internal functions
30 | %%--------------------------------------------------------------------
31 | %% NOTE: This parse works as long as the realm annotation of the
32 | %% Socket.IO protocol is not used. If that annotation is used,
33 | %% it will inject an extra ':', which gums up the whole works.
34 | parse(Data, #socketio_msg{type=undefined} = Message) ->
35 | [TypePart, Rest] = binary:split(Data, <<":">>),
36 | Type = case list_to_integer(binary_to_list(TypePart)) of
37 | 1 -> message
38 | end,
39 | parse(Rest, Message#socketio_msg{type=Type});
40 | parse(Data, #socketio_msg{data=undefined} = Message) ->
41 | [Length, Rest] = binary:split(Data, <<":">>),
42 | [Annotations, Body] = binary:split(Rest, <<":">>),
43 | BodyLength = list_to_integer(binary_to_list(Length)) - size(Annotations),
44 | BodyLength = size(Body),
45 | {ok, Message#socketio_msg{data=binary:part(Body, {0, BodyLength - 1})}}.
46 |
47 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_session_manager.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_session_manager).
2 |
3 | -behaviour(gen_server).
4 |
5 | %% API
6 | -export([
7 | start_link/0,
8 | create/1,
9 | fetch_pid/1
10 | ]).
11 |
12 | %% gen_server callbacks
13 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
14 | terminate/2, code_change/3]).
15 |
16 | -define(SERVER, ?MODULE).
17 | -define(TABLE_ID, ?MODULE).
18 |
19 | -record(state, {next_session_id}).
20 |
21 | %%--------------------------------------------------------------------
22 | %% API
23 | %%--------------------------------------------------------------------
24 |
25 | start_link() ->
26 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
27 |
28 | create(Pid) ->
29 | gen_server:call(?SERVER, {create, Pid}).
30 |
31 | fetch_pid(SessionId) when is_list(SessionId) ->
32 | fetch_pid(list_to_integer(SessionId));
33 | fetch_pid(SessionId) ->
34 | case ets:lookup(?TABLE_ID, SessionId) of
35 | [{SessionId, SessionPid}] -> {ok, SessionPid};
36 | [] -> {error, session_not_found}
37 | end.
38 |
39 | %%--------------------------------------------------------------------
40 | %% gen_server callbacks
41 | %%--------------------------------------------------------------------
42 |
43 | init([]) ->
44 | ets:new(?TABLE_ID, [protected, named_table]),
45 | {ok, #state{next_session_id=1}}.
46 |
47 | handle_call({create, Pid}, _From, State) ->
48 | SessionId = State#state.next_session_id,
49 | {ok, SessionPid} = chloe_session_sup:start_child(Pid),
50 | ets:insert(?TABLE_ID, {SessionId, SessionPid}),
51 | NewState = State#state{next_session_id=SessionId + 1},
52 | {reply, {ok, integer_to_list(SessionId)}, NewState}.
53 |
54 | handle_cast(_Request, State) ->
55 | {noreply, State}.
56 |
57 | handle_info(_Reason, State) ->
58 | {noreply, State}.
59 |
60 | terminate(_Info, _State) ->
61 | ok.
62 |
63 | code_change(_OldVsn, State, _Extra) ->
64 | {ok, State}.
65 |
66 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_xhr_stream.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_xhr_stream).
2 |
3 | -behaviour(gen_server).
4 | -include_lib("./chloe.hrl").
5 |
6 | %% API
7 | -export([
8 | start_link/2,
9 | send/3
10 | ]).
11 |
12 | %% gen_server callbacks
13 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
14 | terminate/2, code_change/3]).
15 |
16 | -record(state, {socket, message, yaws_pid}).
17 |
18 | %%--------------------------------------------------------------------
19 | %% API functions
20 | %%--------------------------------------------------------------------
21 |
22 | start_link(Socket, Message) ->
23 | gen_server:start_link(?MODULE, [Socket, Message], []).
24 |
25 | send(Pid, Channel, Data) ->
26 | gen_server:cast(Pid, {send, [Channel, Data]}).
27 |
28 | %%--------------------------------------------------------------------
29 | %% gen_server callbacks
30 | %%--------------------------------------------------------------------
31 |
32 | init([Socket, Message]) ->
33 | {ok, #state{socket = Socket, message = Message}}.
34 |
35 | handle_call(_Request, _From, State) ->
36 | {reply, ok, State}.
37 |
38 | handle_cast({send, [Messages]}, State) ->
39 | Packed = chloe_message:pack(Messages),
40 | yaws_api:stream_process_deliver_final_chunk(State#state.socket, Packed),
41 | yaws_api:stream_process_end(State#state.socket, State#state.yaws_pid),
42 | {stop, normal, State}.
43 |
44 | handle_info({ok, YawsPid}, State) ->
45 | Message = State#state.message,
46 | {ok, SessionPid} = chloe_session_manager:fetch_pid(Message#message.session_id),
47 | %% Now that Yaws is ready, we attach ourselves to our session.
48 | chloe_session:attach_transport_pid(SessionPid, self()),
49 | {noreply, State#state{yaws_pid = YawsPid}};
50 | handle_info({discard, YawsPid}, State) ->
51 | yaws_api:stream_process_end(State#state.socket, YawsPid),
52 | {stop, normal, State}.
53 |
54 | terminate(_Reason, _State) ->
55 | ok.
56 |
57 | code_change(_OldVsn, State, _Extra) ->
58 | {ok, State}.
59 |
--------------------------------------------------------------------------------
/javascripts/chloe-client.js:
--------------------------------------------------------------------------------
1 | Chloe = function (options) {
2 | options = options || {};
3 | options.host = options.host || 'localhost';
4 | options.port = options.port || 8901;
5 |
6 | var transports = options.transports ||
7 | [Chloe.WebSocketTransport,
8 | // TODO (trotter): Test XHR and add back the transport
9 | // Chloe.XhrTransport,
10 | Chloe.JsonpTransport];
11 |
12 | for (var i = 0, l = transports.length; i < l; i++) {
13 | if (transports[i].isEnabled()) {
14 | this.transport = new transports[i](options);
15 | break;
16 | }
17 | }
18 |
19 | this.channelSubscriptions = {};
20 | };
21 |
22 | Chloe.extend = function (source, obj) {
23 | for (var prop in source) obj[prop] = source[prop];
24 | return obj;
25 | };
26 |
27 | Chloe.prototype = {
28 | // Public API
29 | connect: function (callback) {
30 | var self = this;
31 | this.transport.connect(function (data) {
32 | self.sessionId = data.sessionId;
33 | callback();
34 | });
35 | this.transport.onmessage(function (message) {
36 | self.handleMessage(Chloe.Message.unpack(message));
37 | });
38 | },
39 | onmessage: function (callback) {
40 | var self = this;
41 | this.onmessageCallback = callback;
42 | },
43 | onclose: function (callback) {
44 | this.transport.onclose(callback);
45 | },
46 | send: function (data) {
47 | var message = Chloe.Message.pack(data, this.sessionId);
48 | message.send(this.transport);
49 | },
50 | subscribe: function (channel, callback) {
51 | var message = Chloe.Message.channelSubscribe(channel, this);
52 | this.channelSubscriptions[channel] = callback;
53 | message.send(this.transport);
54 | },
55 |
56 | // Internal functions
57 | handleMessage: function (message) {
58 | var callback = this.channelSubscriptions[message.channel];
59 | if (callback) {
60 | callback(message.data);
61 | } else if (this.onmessageCallback) {
62 | this.onmessageCallback(message.data);
63 | }
64 | }
65 | };
66 |
--------------------------------------------------------------------------------
/javascripts/chloe-message.js:
--------------------------------------------------------------------------------
1 | // Chloe message types:
2 | // 'connect'
3 | // 'channel-subscribe'
4 | // 'message'
5 | // 'poll'
6 |
7 | Chloe.Message = function (options) {
8 | this.version = Chloe.Message.version;
9 | this.sessionId = options.sessionId;
10 | this.id = options.id;
11 | this.type = options.type;
12 | // TODO (trotter): I don't really like doing this, find a better way.
13 | this.channel = options.channel;
14 | this.data = options.data;
15 | this.packed = options.packed;
16 | };
17 |
18 | Chloe.Message.version = 1;
19 |
20 | Chloe.Message.pack = function (data, sessionId) {
21 | var message = new Chloe.Message({data: data,
22 | type: "message",
23 | sessionId: sessionId});
24 | message.pack();
25 | return message;
26 | };
27 |
28 | Chloe.Message.unpack = function (packed) {
29 | var message = new Chloe.Message({packed: packed});
30 | message.unpack();
31 | return message;
32 | };
33 |
34 | Chloe.Message.channelSubscribe = function (channel, client) {
35 | var message = new Chloe.Message({type: "channel-subscribe",
36 | sessionId: client.sessionId,
37 | channel: channel});
38 | message.pack();
39 | return message;
40 | };
41 |
42 | Chloe.Message.prototype = {
43 | pack: function () {
44 | this.packed = JSON.stringify({
45 | type: this.type,
46 | channel: this.channel,
47 | data: this.data,
48 | version: this.version,
49 | id: this.id,
50 | sessionId: this.sessionId
51 | });
52 | },
53 | unpack: function () {
54 | var decoded = JSON.parse(this.packed);
55 | if (decoded.version !== this.version) {
56 | throw new Error("Expected message version " + decoded.version + " to match " + this.version);
57 | }
58 | this.data = decoded.data;
59 | this.channel = decoded.channel;
60 | this.type = decoded.type;
61 | this.id = decoded.id;
62 | this.sessionId = decoded.sessionId;
63 | },
64 | send: function (transport) {
65 | this.pack();
66 | transport.send(this.packed);
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_channel_store.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_channel_store).
2 |
3 | -behaviour(gen_server).
4 |
5 | -export([
6 | start_link/0,
7 | subscribe/2,
8 | fetch_subscribers/1
9 | ]).
10 |
11 | %% gen_server callbacks
12 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
13 | terminate/2, code_change/3]).
14 |
15 | -define(SERVER, ?MODULE).
16 | -define(TABLE_ID, ?MODULE).
17 |
18 | -record(state, {}).
19 |
20 | %%--------------------------------------------------------------------
21 | %% API
22 | %%--------------------------------------------------------------------
23 |
24 | start_link() ->
25 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
26 |
27 | subscribe(Channel, Pid) ->
28 | gen_server:cast(?SERVER, {subscribe, [Channel, Pid]}).
29 |
30 | fetch_subscribers(Channel) ->
31 | gen_server:call(?SERVER, {fetch_subscribers, Channel}).
32 |
33 | %%--------------------------------------------------------------------
34 | %% gen_server callbacks
35 | %%--------------------------------------------------------------------
36 |
37 | init([]) ->
38 | ets:new(?TABLE_ID, [protected, named_table]),
39 | {ok, #state{}}.
40 |
41 | handle_call({fetch_subscribers, Channel}, _From, State) ->
42 | Response = subscribers_for(Channel),
43 | {reply, Response, State}.
44 |
45 | handle_cast({subscribe, [Channel, Pid]}, State) ->
46 | OldSubscribers = case subscribers_for(Channel) of
47 | {ok, Subscribers} -> Subscribers;
48 | _ -> []
49 | end,
50 | NewSubscribers = add_subscriber(Pid, OldSubscribers),
51 | ets:insert(?TABLE_ID, {Channel, NewSubscribers}),
52 | {noreply, State}.
53 |
54 | handle_info(_Reason, State) ->
55 | {noreply, State}.
56 |
57 | terminate(_Info, _State) ->
58 | ok.
59 |
60 | code_change(_OldVsn, State, _Extra) ->
61 | {ok, State}.
62 |
63 | %%--------------------------------------------------------------------
64 | %% internal functions
65 | %%--------------------------------------------------------------------
66 |
67 | add_subscriber(NewSubscriber, Subscribers) ->
68 | [NewSubscriber | lists:delete(NewSubscriber, Subscribers)].
69 |
70 | subscribers_for(Channel) ->
71 | case ets:lookup(?TABLE_ID, Channel) of
72 | [{Channel, Subscribers}] -> {ok, Subscribers};
73 | [] -> {ok, []}
74 | end.
75 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_yaws_xhr.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_yaws_xhr).
2 |
3 | -include("../../../deps/yaws/include/yaws_api.hrl").
4 | -include_lib("./chloe.hrl").
5 |
6 | -export([out/1]).
7 |
8 | -define(MIME_TYPE, "application/json").
9 |
10 | %%--------------------------------------------------------------------
11 | %% API
12 | %%--------------------------------------------------------------------
13 |
14 | out(A) ->
15 | {ok, Raw} = yaws_api:getvar(A, "data"),
16 | Message = chloe_message:unpack(Raw),
17 | case Message#message.type of
18 | "connect" -> handle_connect(Message);
19 | "message" -> handle_message(Message);
20 | "channel-subscribe" -> handle_channel_subscribe(Message);
21 | "poll" -> handle_poll(A, Message)
22 | end.
23 |
24 | %%--------------------------------------------------------------------
25 | %% Internal functions
26 | %%--------------------------------------------------------------------
27 |
28 | cross_origin_response() ->
29 | cross_origin_response("").
30 |
31 | %% TODO (trotter): May need to append the port here, I noticed
32 | %% that Chrome was upset with this header.
33 | cross_origin_response(Response) when is_tuple(Response) ->
34 | {ok, Origin} = application:get_env(chloe, application_server),
35 | [{header, ["Access-Control-Allow-Origin: ", Origin]},
36 | Response];
37 |
38 | cross_origin_response(Packed) ->
39 | cross_origin_response({content, ?MIME_TYPE, Packed}).
40 |
41 | handle_connect(Message) ->
42 | SessionId = create_session(),
43 | Packed = chloe_message:pack(#message{session_id=SessionId,
44 | id=Message#message.id,
45 | type=Message#message.type}),
46 | cross_origin_response(Packed).
47 |
48 | handle_message(Message) ->
49 | chloe_session:send_to_server(session_pid(Message#message.session_id),
50 | Message#message.data),
51 | cross_origin_response().
52 |
53 | handle_channel_subscribe(Message) ->
54 | chloe_session:subscribe(session_pid(Message#message.session_id),
55 | Message#message.channel),
56 | cross_origin_response().
57 |
58 | handle_poll(A, Message) ->
59 | {ok, StreamPid} = chloe_xhr_stream_sup:start_child(A#arg.clisock, Message),
60 | cross_origin_response({streamcontent_from_pid, ?MIME_TYPE, StreamPid}).
61 |
62 | session_pid(SessionId) ->
63 | {ok, SessionPid} = chloe_session_manager:fetch_pid(SessionId),
64 | SessionPid.
65 |
66 | create_session() ->
67 | %% TODO: We should really tell chloe session manager what it's dealing
68 | %% with, so that it doesn't try to send data down the pipe.
69 | {ok, SessionId} = chloe_session_manager:create(undefined),
70 | SessionId.
71 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_jsonp_stream.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_jsonp_stream).
2 |
3 | -behaviour(gen_server).
4 | -include_lib("./chloe.hrl").
5 |
6 | %% API
7 | -export([
8 | start_link/2,
9 | send/3
10 | ]).
11 |
12 | %% gen_server callbacks
13 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
14 | terminate/2, code_change/3]).
15 |
16 | -record(state, {socket, message, yaws_pid}).
17 |
18 | %%--------------------------------------------------------------------
19 | %% API functions
20 | %%--------------------------------------------------------------------
21 |
22 | start_link(Socket, Message) ->
23 | gen_server:start_link(?MODULE, [Socket, Message], []).
24 |
25 | send(Pid, Channel, Data) ->
26 | gen_server:cast(Pid, {send, [Channel, Data]}).
27 |
28 | %%--------------------------------------------------------------------
29 | %% gen_server callbacks
30 | %%--------------------------------------------------------------------
31 |
32 | init([Socket, Message]) ->
33 | {ok, #state{socket = Socket, message = Message}}.
34 |
35 | handle_call(_Request, _From, State) ->
36 | {reply, ok, State}.
37 |
38 | handle_cast({send, [Messages]}, State) ->
39 | Message = State#state.message,
40 | PackedMessages = lists:map(fun (M) ->
41 | Packed = chloe_message:pack(#message{id=Message#message.id,
42 | type=Message#message.type,
43 | data=M#message.data,
44 | channel=M#message.channel}),
45 | jsonp_response(Packed)
46 | end, Messages),
47 | Packed = string:join(PackedMessages, ""),
48 | yaws_api:stream_process_deliver_final_chunk(State#state.socket, Packed),
49 | yaws_api:stream_process_end(State#state.socket, State#state.yaws_pid),
50 | error_logger:info_msg("Should be sending data now!!"),
51 | {stop, normal, State}.
52 |
53 | handle_info({ok, YawsPid}, State) ->
54 | Message = State#state.message,
55 | {ok, SessionPid} = chloe_session_manager:fetch_pid(Message#message.session_id),
56 | %% Now that Yaws is ready, we attach ourselves to our session.
57 | chloe_session:attach_transport_pid(SessionPid, self()),
58 | {noreply, State#state{yaws_pid = YawsPid}};
59 | handle_info({discard, YawsPid}, State) ->
60 | yaws_api:stream_process_end(State#state.socket, YawsPid),
61 | {stop, normal, State}.
62 |
63 | terminate(_Reason, _State) ->
64 | ok.
65 |
66 | code_change(_OldVsn, State, _Extra) ->
67 | {ok, State}.
68 |
69 | %%--------------------------------------------------------------------
70 | %% Internal functions
71 | %%--------------------------------------------------------------------
72 |
73 | jsonp_response(Packed) ->
74 | string:join(["Chloe.JsonpTransport.response(", Packed, ");"], "").
75 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'fileutils'
3 | require 'bundler'
4 | Bundler.require
5 |
6 | desc "Install all necessary dependencies"
7 | task :bootstrap do
8 | sh("bundle install")
9 | sh("rebar get-deps")
10 | end
11 |
12 | desc "Start an erlang console"
13 | task :console => :compile do
14 | sh(erl)
15 | end
16 |
17 | desc "Start an erlang console running chloe"
18 | task :server => :compile do
19 | sh(erl "-s chloe -config ./rel/files/app")
20 | end
21 |
22 | desc "Compile Chloe"
23 | task :compile => [:build_js, :test] do
24 | sh("rebar update-deps")
25 | sh("rebar compile")
26 | end
27 |
28 | desc "Run unit tests for chloe"
29 | task :test do
30 | sh("rebar app=chloe eunit")
31 | end
32 |
33 | desc "Clean up"
34 | task :clean do
35 | sh "rebar clean"
36 | end
37 |
38 | desc "Generate a release on this box"
39 | task :platform_release => [:clean, :compile] do
40 | # TODO (factor out version to not be hard coded)
41 | version = "0.0.5"
42 | FileUtils.rm_rf("./rel/chloe")
43 | FileUtils.rm_rf("./rel/chloe-#{version}")
44 | sh "rebar generate"
45 | sh "cp -r ./rel/chloe ./rel/chloe-#{version}"
46 | sh "cd ./rel && tar czf chloe-#{version}.tgz chloe-#{version}"
47 | end
48 |
49 | desc "Run demo echo server"
50 | task :demo do
51 | require './support/echo_server'
52 | Sinatra::Application.run!
53 | end
54 |
55 | desc "Build chloe.js"
56 | task :build_js do
57 | unminified = "public/chloe.js"
58 |
59 | secretary = Sprockets::Secretary.new(
60 | :asset_root => "public",
61 | :source_files => ["javascripts/chloe.js"]
62 | )
63 |
64 | secretary.concatenation.save_to(unminified)
65 |
66 | File.open("public/chloe-min.js", "w") do |f|
67 | f.write Uglifier.new.compile(File.read(unminified))
68 | end
69 | end
70 |
71 | begin
72 | require('jslintrb-v8')
73 | task :jslint do
74 | linter = JSLint.new(
75 | :white => false,
76 | :undef => true,
77 | :nomen => false,
78 | :eqeqeq => true,
79 | :plusplus => true,
80 | :bitwise => true,
81 | :regexp => false,
82 | :strict => false,
83 | :newcap => true,
84 | :immed => true,
85 | :indent => 2,
86 | :predef => "Chloe"
87 | )
88 | errors = []
89 | path = File.join('public', '**', '*.js')
90 | Dir[path].each do |f|
91 | puts "checking #{f}"
92 | e = linter.check(File.read(f))
93 | errors << "\nIn [#{f}]:\n#{e}\n" if e
94 | end
95 | if errors.empty?
96 | puts "JSLinty-fresh!"
97 | else
98 | $stderr.write(errors.join("\n")+"\n");
99 | raise "JSLint Errors Found"
100 | end
101 | end
102 | rescue LoadError
103 | puts "jslintrb_v8 not installed. Not adding jslint task"
104 | end
105 |
106 | def erl(extra="")
107 | "erl -pa apps/chloe/ebin -pa deps/yaws/ebin #{extra}"
108 | end
109 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_yaws_jsonp.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_yaws_jsonp).
2 |
3 | -include("../../../deps/yaws/include/yaws_api.hrl").
4 | -include_lib("./chloe.hrl").
5 |
6 | -export([out/1]).
7 |
8 | -define(MIME_TYPE, "application/javascript").
9 |
10 | %%--------------------------------------------------------------------
11 | %% API
12 | %%--------------------------------------------------------------------
13 |
14 | out(A) ->
15 | Raw = proplists:get_value("data", yaws_api:parse_query(A)),
16 | Message = chloe_message:unpack(Raw),
17 | case Message#message.type of
18 | "connect" -> handle_connect(Message);
19 | "message" -> handle_message(Message);
20 | "channel-subscribe" -> handle_channel_subscribe(Message);
21 | "poll" -> handle_poll(A, Message)
22 | end.
23 |
24 | %%--------------------------------------------------------------------
25 | %% Internal functions
26 | %%--------------------------------------------------------------------
27 |
28 | handle_connect(Message) ->
29 | SessionId = create_session(),
30 | Packed = chloe_message:pack(#message{session_id=SessionId,
31 | id=Message#message.id,
32 | type=Message#message.type}),
33 | {content, ?MIME_TYPE, jsonp_response(Packed)}.
34 |
35 | handle_message(Message) ->
36 | chloe_session:send_to_server(session_pid(Message#message.session_id),
37 | Message#message.data),
38 | {content, ?MIME_TYPE, ""}.
39 |
40 | handle_channel_subscribe(Message) ->
41 | chloe_session:subscribe(session_pid(Message#message.session_id),
42 | Message#message.channel),
43 | {content, ?MIME_TYPE, ""}.
44 |
45 | handle_poll(A, Message) ->
46 | {ok, JsonpStreamPid} = chloe_jsonp_stream_sup:start_child(A#arg.clisock, Message),
47 | {streamcontent_from_pid, ?MIME_TYPE, JsonpStreamPid}.
48 |
49 | %% Messages = chloe_session:retrieve_messages(session_pid(Message#message.session_id)),
50 | %% error_logger:info_msg("We got messages ~p~n", [Messages]),
51 | %% PackedMessages = lists:map(fun (M) ->
52 | %% Packed = chloe_message:pack(#message{id=Message#message.id,
53 | %% type=Message#message.type,
54 | %% data=M#message.data,
55 | %% channel=M#message.channel}),
56 | %% jsonp_response(Packed)
57 | %% end, Messages),
58 | %% Packed = string:join(PackedMessages, ""),
59 | %% {content, "application/javascript", Packed}.
60 |
61 | jsonp_response(Packed) ->
62 | string:join(["Chloe.JsonpTransport.response(", Packed, ");"], "").
63 |
64 | session_pid(SessionId) ->
65 | {ok, SessionPid} = chloe_session_manager:fetch_pid(SessionId),
66 | SessionPid.
67 |
68 | create_session() ->
69 | %% TODO: We should really tell chloe session manager what it's dealing
70 | %% with, so that it doesn't try to send data down the pipe.
71 | {ok, SessionId} = chloe_session_manager:create(undefined),
72 | SessionId.
73 |
--------------------------------------------------------------------------------
/rel/files/nodetool:
--------------------------------------------------------------------------------
1 | %% -*- erlang -*-
2 | %% -------------------------------------------------------------------
3 | %%
4 | %% nodetool: Helper Script for interacting with live nodes
5 | %%
6 | %% -------------------------------------------------------------------
7 |
8 | main(Args) ->
9 | %% Extract the args
10 | {RestArgs, TargetNode} = process_args(Args, [], undefined),
11 |
12 | %% See if the node is currently running -- if it's not, we'll bail
13 | case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of
14 | {true, pong} ->
15 | ok;
16 | {_, pang} ->
17 | io:format("Node ~p not responding to pings.\n", [TargetNode]),
18 | halt(1)
19 | end,
20 |
21 | case RestArgs of
22 | ["ping"] ->
23 | %% If we got this far, the node already responsed to a ping, so just dump
24 | %% a "pong"
25 | io:format("pong\n");
26 | ["stop"] ->
27 | io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]);
28 | ["restart"] ->
29 | io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]);
30 | ["reboot"] ->
31 | io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]);
32 | ["rpc", Module, Function | RpcArgs] ->
33 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), [RpcArgs], 60000) of
34 | ok ->
35 | ok;
36 | {badrpc, Reason} ->
37 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]),
38 | halt(1);
39 | _ ->
40 | halt(1)
41 | end;
42 | Other ->
43 | io:format("Other: ~p\n", [Other]),
44 | io:format("Usage: nodetool {ping|stop|restart|reboot}\n")
45 | end,
46 | net_kernel:stop().
47 |
48 | process_args([], Acc, TargetNode) ->
49 | {lists:reverse(Acc), TargetNode};
50 | process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) ->
51 | erlang:set_cookie(node(), list_to_atom(Cookie)),
52 | process_args(Rest, Acc, TargetNode);
53 | process_args(["-name", TargetName | Rest], Acc, _) ->
54 | ThisNode = append_node_suffix(TargetName, "_maint_"),
55 | {ok, _} = net_kernel:start([ThisNode, longnames]),
56 | process_args(Rest, Acc, nodename(TargetName));
57 | process_args(["-sname", TargetName | Rest], Acc, _) ->
58 | ThisNode = append_node_suffix(TargetName, "_maint_"),
59 | {ok, _} = net_kernel:start([ThisNode, shortnames]),
60 | process_args(Rest, Acc, nodename(TargetName));
61 | process_args([Arg | Rest], Acc, Opts) ->
62 | process_args(Rest, [Arg | Acc], Opts).
63 |
64 |
65 | nodename(Name) ->
66 | case string:tokens(Name, "@") of
67 | [_Node, _Host] ->
68 | list_to_atom(Name);
69 | [Node] ->
70 | [_, Host] = string:tokens(atom_to_list(node()), "@"),
71 | list_to_atom(lists:concat([Node, "@", Host]))
72 | end.
73 |
74 | append_node_suffix(Name, Suffix) ->
75 | case string:tokens(Name, "@") of
76 | [Node, Host] ->
77 | list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host]));
78 | [Node] ->
79 | list_to_atom(lists:concat([Node, Suffix, os:getpid()]))
80 | end.
81 |
--------------------------------------------------------------------------------
/javascripts/chloe-jsonp.js:
--------------------------------------------------------------------------------
1 | Chloe.JsonpTransport = function (options) {
2 | this.host = options.host;
3 | this.port = options.port;
4 | this.protocol = "http://";
5 | this.callbacks = {};
6 | this.register();
7 | };
8 |
9 | Chloe.JsonpTransport.prototype = {
10 | // Public API
11 | connect: function (callback) {
12 | var self = this,
13 | message = new Chloe.Message({
14 | id: this.id,
15 | type: 'connect'
16 | });
17 |
18 | this.callbacks.onconnect = function (data) {
19 | // TODO: Storing sessionId both here and on client level.
20 | // Feels marginally wrong.
21 | self.sessionId = data.sessionId;
22 | self.listenForMessages();
23 | callback(data);
24 | };
25 |
26 | message.pack();
27 | message.send(this);
28 | },
29 |
30 | onmessage: function (callback) {
31 | this.callbacks.onmessage = callback;
32 | },
33 |
34 | onclose: function (callback) {
35 | this.callbacks.onclose = callback;
36 | },
37 |
38 | send: function (data, options) {
39 | var self = this,
40 | script = document.createElement('script');
41 |
42 | options = options || {};
43 |
44 | script.src = this.url(data);
45 | script.type = 'text/javascript';
46 | script.onerror = function () {
47 | if (options.onerror) {
48 | options.onerror();
49 | }
50 | // TODO: Find out what, if any, arguments this takes
51 | self.handleError();
52 | };
53 | document.body.appendChild(script);
54 | },
55 |
56 | // Internal functions
57 | register: function () {
58 | this.id = (new Date()).getTime();
59 | Chloe.JsonpTransport.connections[this.id] = this;
60 | },
61 |
62 | url: function (data) {
63 | return this.protocol + this.host + ":" + this.port + "/chloe/jsonp.js?data=" + escape(data) + "ts=" + (new Date()).getTime();
64 | },
65 |
66 | handleError: function () {
67 | // Need to figure out what to do here.
68 | },
69 |
70 | listenForMessages: function () {
71 | var self = this,
72 | message = new Chloe.Message({
73 | id: this.id,
74 | sessionId: this.sessionId,
75 | type: "poll"
76 | });
77 | message.send(this, {onerror: function () { self.listenForMessages(); }});
78 | }
79 | };
80 |
81 | Chloe.JsonpTransport.connections = {};
82 |
83 | Chloe.JsonpTransport.isEnabled = function () {
84 | return true;
85 | }
86 |
87 | Chloe.JsonpTransport.response = function (data) {
88 | var message = new Chloe.Message(data),
89 | connection = Chloe.JsonpTransport.connections[message.id];
90 |
91 | // TODO: We are packing because chloe-client.js is going to try to unpack
92 | // later. We need to remove this dependency and instead have the transports
93 | // take care of unpacking.
94 | message.pack();
95 | if (message.type === 'connect') {
96 | connection.callbacks.onconnect(message);
97 | } else if (message.type === 'poll') {
98 | connection.callbacks.onmessage(message.packed);
99 | connection.listenForMessages();
100 | } else {
101 | throw new Error("Unknown message type for JsonpTransport.");
102 | }
103 | };
104 |
--------------------------------------------------------------------------------
/javascripts/chloe-xhr.js:
--------------------------------------------------------------------------------
1 | Chloe.XhrTransport = function (options) {
2 | this.host = options.host;
3 | this.port = options.port;
4 | this.protocol = "http://";
5 | this.callbacks = {};
6 | };
7 |
8 | Chloe.XhrTransport.isEnabled = function (host) {
9 | return 'XMLHttpRequest' in window &&
10 | this.prototype.makeXhr().withCredentials != undefined;
11 | };
12 |
13 | Chloe.XhrTransport.prototype = {
14 | makeXhr: function () {
15 | return new XMLHttpRequest();
16 | },
17 |
18 | url: function (path) {
19 | return this.protocol + this.host + ":" + this.port + "/chloe" + path;
20 | },
21 |
22 | connect: function (callback) {
23 | var self = this,
24 | message = new Chloe.Message({
25 | type: 'connect'
26 | });
27 |
28 | message.pack();
29 | this.postRequest(message.packed, function (data) {
30 | self.sessionId = data.sessionId;
31 | self.listenForMessages();
32 | callback(message);
33 | });
34 | },
35 |
36 | send: function (outbound) {
37 | // TODO (mat): definitely need to move pack/unpack into the transport
38 | var message = Chloe.Message.unpack(outbound);
39 | message.sessionId = this.sessionId;
40 | message.pack();
41 | this.postRequest(message.packed);
42 | },
43 |
44 | onclose: function (callback) {
45 | this.callbacks.onclose = function () {
46 | clearTimeout(this.poller);
47 | callback();
48 | };
49 | },
50 |
51 | onmessage: function (callback) {
52 | this.callbacks.onmessage = callback;
53 | },
54 |
55 | noop: function () {
56 | },
57 |
58 | handleStateChange: function (req, callback) {
59 | var received = callback || this.noop,
60 | closed = this.callbacks.onclose || this.noop;
61 | req.onreadystatechange = function(){
62 | var message, status;
63 | if (req.readyState == 4){
64 | req.onreadystatechange = this.noop;
65 | try { status = req.status; } catch(e){}
66 | if (status == 200){
67 | if (req.responseText !== "") {
68 | var data = JSON.parse(req.responseText);
69 | if (data.messages) {
70 | var messages = data.messages;
71 | for (var i in messages) {
72 | received(new Chloe.Message(messages[i]));
73 | }
74 | } else {
75 | received(new Chloe.Message(data));
76 | }
77 | }
78 | } else {
79 | closed();
80 | }
81 | }
82 | }
83 | },
84 |
85 | getRequest: function (callback) {
86 | var req = this.makeXhr(),
87 | message = new Chloe.Message({ sessionId: this.sessionId,
88 | type: "poll" });
89 | message.pack();
90 | req.open('GET', this.url("/xhr/" + (+ new Date)) +
91 | "?data=" + escape(message.packed));
92 | this.handleStateChange(req, callback);
93 | req.send(null);
94 | },
95 |
96 | postRequest: function (data, callback) {
97 | var req = this.makeXhr();
98 | req.open('POST', this.url('/xhr'));
99 | if ('setRequestHeader' in req) {
100 | req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=utf-8');
101 | }
102 | this.handleStateChange(req, callback);
103 | req.send("data=" + escape(data));
104 | },
105 |
106 | listenForMessages: function () {
107 | var self = this,
108 | onmessage = this.callbacks.onmessage || this.noop;
109 | message = new Chloe.Message({ sessionId: this.sessionId,
110 | type: "poll" });
111 |
112 | this.getRequest(function (incoming) {
113 | if (typeof(incoming) !== "undefined") {
114 | incoming.pack();
115 | onmessage(incoming.packed);
116 | }
117 | // XXX (trotter): The following works for fetching messages,
118 | // but it causes a weird '' error message
119 | // to show up in firebug.
120 | self.listenForMessages();
121 | });
122 | }
123 | };
124 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_websocket.erl:
--------------------------------------------------------------------------------
1 | %% To test run the following in chrome:
2 | %% ws = new WebSocket("ws://localhost:8888/updates");
3 | %% ws.onmessage = function (m) { console.log(m.data); };
4 | %% ws.send("Hey dude");
5 |
6 | -module(chloe_websocket).
7 |
8 | -behaviour(gen_server).
9 |
10 | %% API
11 | -export([
12 | start_link/0,
13 | send/2
14 | ]).
15 |
16 | %% gen_server callbacks
17 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
18 | terminate/2, code_change/3]).
19 |
20 | -record(state, {websocket, session_id}).
21 | -include_lib("./chloe.hrl").
22 |
23 | %%--------------------------------------------------------------------
24 | %% API
25 | %%--------------------------------------------------------------------
26 |
27 | start_link() ->
28 | gen_server:start_link(?MODULE, [], []).
29 |
30 | send(Pid, [Messages]) ->
31 | gen_server:cast(Pid, {send, [Messages]}).
32 |
33 | %%--------------------------------------------------------------------
34 | %% gen_server callbacks
35 | %%--------------------------------------------------------------------
36 |
37 | init([]) ->
38 | {ok, #state{}}.
39 |
40 | handle_call(_Request, _From, State) ->
41 | {reply, ok, State}.
42 |
43 | handle_cast({send, [Messages]}, State) ->
44 | lists:foreach(fun (M) ->
45 | Packed = chloe_message:pack(M),
46 | error_logger:info_msg("Sending back: ~p", [Packed]),
47 | yaws_api:websocket_send(State#state.websocket, Packed)
48 | end, Messages),
49 | {noreply, State}.
50 |
51 | %% This is where our websocket comms will come in
52 | handle_info({ok, WebSocket}, State) ->
53 | error_logger:info_msg("Websocket started on ~p~n", [self()]),
54 | SessionId = perform_session_handshake(WebSocket),
55 | {noreply, State#state{websocket = WebSocket,
56 | session_id = SessionId}};
57 | handle_info({tcp, _WebSocket, DataFrames}, State) ->
58 | error_logger:info_msg("Raw DataFrame: ~p~n", [DataFrames]),
59 | handle_websocket_frames(DataFrames, State),
60 | {noreply, State};
61 | handle_info({tcp_closed, _WebSocket}, State) ->
62 | {stop, ok, State};
63 | % handle_info(discard, State) ->
64 | % {stop, ok, State};
65 | %% httpc is going to tell us how our request went,
66 | %% but we don't care
67 | handle_info({http, _Response}, State) ->
68 | {noreply, State}.
69 |
70 | terminate(_Reason, _State) ->
71 | ok.
72 |
73 | code_change(_OldVsn, State, _Extra) ->
74 | {ok, State}.
75 |
76 | %%--------------------------------------------------------------------
77 | %% internal functions
78 | %%--------------------------------------------------------------------
79 |
80 | perform_session_handshake(_WebSocket) ->
81 | {ok, SessionId} = chloe_session_manager:create(self()),
82 | % Message = chloe_socketio_protocol:pack(handshake, SessionId),
83 | % yaws_api:websocket_send(WebSocket, Message),
84 | SessionId.
85 |
86 | session_pid(SessionId) ->
87 | {ok, SessionPid} = chloe_session_manager:fetch_pid(SessionId),
88 | SessionPid.
89 |
90 | handle_message(Data, State) ->
91 | Message = chloe_message:unpack(Data),
92 | case Message#message.type of
93 | "channel-subscribe" -> handle_channel_subscribe_message(Message, State);
94 | _ -> handle_data_message(Message, State)
95 | end.
96 |
97 | handle_websocket_frames(DataFrames, State) ->
98 | %% TODO (trotter): We _may_ be able to use yaws_websockets:unframe_all
99 | %% then process the resulting list.
100 | case yaws_websockets:unframe_one(DataFrames) of
101 | {ok, Data, <<>>} -> handle_message(Data, State);
102 | {ok, Data, NextFrame} -> handle_message(Data, State),
103 | handle_websocket_frames(NextFrame, State)
104 | end.
105 |
106 | handle_channel_subscribe_message(Message, State) ->
107 | chloe_session:subscribe(session_pid(State#state.session_id),
108 | Message#message.channel).
109 |
110 | handle_data_message(Message, State) ->
111 | error_logger:info_msg("Got data from WebSocket: ~p~n", [Message#message.data]),
112 | chloe_session:send_to_server(session_pid(State#state.session_id),
113 | Message#message.data).
114 |
--------------------------------------------------------------------------------
/rel/files/chloe:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # -*- tab-width:4;indent-tabs-mode:nil -*-
3 | # ex: ts=4 sw=4 et
4 |
5 | RUNNER_SCRIPT_DIR=$(cd ${0%/*} && pwd)
6 |
7 | RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*}
8 | RUNNER_ETC_DIR=$RUNNER_BASE_DIR/etc
9 | RUNNER_LOG_DIR=$RUNNER_BASE_DIR/log
10 | PIPE_DIR=/tmp/$RUNNER_BASE_DIR/
11 | RUNNER_USER=
12 |
13 | # Make sure this script is running as the appropriate user
14 | if [ ! -z "$RUNNER_USER" ] && [ `whoami` != "$RUNNER_USER" ]; then
15 | exec sudo -u $RUNNER_USER -i $0 $@
16 | fi
17 |
18 | # Make sure CWD is set to runner base dir
19 | cd $RUNNER_BASE_DIR
20 |
21 | # Make sure log directory exists
22 | mkdir -p $RUNNER_LOG_DIR
23 |
24 | # Extract the target node name from node.args
25 | NAME_ARG=`grep -e '-[s]*name' $RUNNER_ETC_DIR/vm.args`
26 | if [ -z "$NAME_ARG" ]; then
27 | echo "vm.args needs to have either -name or -sname parameter."
28 | exit 1
29 | fi
30 |
31 | # Extract the target cookie
32 | COOKIE_ARG=`grep -e '-setcookie' $RUNNER_ETC_DIR/vm.args`
33 | if [ -z "$COOKIE_ARG" ]; then
34 | echo "vm.args needs to have a -setcookie parameter."
35 | exit 1
36 | fi
37 |
38 | # Identify the script name
39 | SCRIPT=`basename $0`
40 |
41 | # Parse out release and erts info
42 | START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data`
43 | ERTS_VSN=${START_ERL% *}
44 | APP_VSN=${START_ERL#* }
45 |
46 | # Add ERTS bin dir to our path
47 | ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin
48 |
49 | # Setup command to control the node
50 | NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG"
51 |
52 | # Check the first argument for instructions
53 | case "$1" in
54 | start)
55 | # Make sure there is not already a node running
56 | RES=`$NODETOOL ping`
57 | if [ "$RES" = "pong" ]; then
58 | echo "Node is already running!"
59 | exit 1
60 | fi
61 | HEART_COMMAND="$RUNNER_BASE_DIR/bin/$SCRIPT start"
62 | export HEART_COMMAND
63 | mkdir -p $PIPE_DIR
64 | # Note the trailing slash on $PIPE_DIR/
65 | $ERTS_PATH/run_erl -daemon $PIPE_DIR/ $RUNNER_LOG_DIR "exec $RUNNER_BASE_DIR/bin/$SCRIPT console" 2>&1
66 | ;;
67 |
68 | stop)
69 | # Wait for the node to completely stop...
70 | case `uname -s` in
71 | Linux|Darwin|FreeBSD|DragonFly|NetBSD|OpenBSD)
72 | # PID COMMAND
73 | PID=`ps ax -o pid= -o command=|\
74 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'`
75 | ;;
76 | SunOS)
77 | # PID COMMAND
78 | PID=`ps -ef -o pid= -o args=|\
79 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'`
80 | ;;
81 | CYGWIN*)
82 | # UID PID PPID TTY STIME COMMAND
83 | PID=`ps -efW|grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $2}'`
84 | ;;
85 | esac
86 | $NODETOOL stop
87 | while `kill -0 $PID 2>/dev/null`;
88 | do
89 | sleep 1
90 | done
91 | ;;
92 |
93 | restart)
94 | ## Restart the VM without exiting the process
95 | $NODETOOL restart
96 | ;;
97 |
98 | reboot)
99 | ## Restart the VM completely (uses heart to restart it)
100 | $NODETOOL reboot
101 | ;;
102 |
103 | ping)
104 | ## See if the VM is alive
105 | $NODETOOL ping
106 | ;;
107 |
108 | attach)
109 | # Make sure a node IS running
110 | RES=`$NODETOOL ping`
111 | if [ "$RES" != "pong" ]; then
112 | echo "Node is not running!"
113 | exit 1
114 | fi
115 |
116 | shift
117 | $ERTS_PATH/to_erl $PIPE_DIR
118 | ;;
119 |
120 | console)
121 | # Setup beam-required vars
122 | ROOTDIR=$RUNNER_BASE_DIR
123 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin
124 | EMU=beam
125 | PROGNAME=`echo $0 | sed 's/.*\\///'`
126 | CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$SCRIPT -embedded -config $RUNNER_ETC_DIR/app.config -args_file $RUNNER_ETC_DIR/vm.args -- ${1+"$@"}"
127 | export EMU
128 | export ROOTDIR
129 | export BINDIR
130 | export PROGNAME
131 |
132 | # Dump environment info for logging purposes
133 | echo "Exec: $CMD"
134 | echo "Root: $ROOTDIR"
135 |
136 | # Log the startup
137 | logger -t "$SCRIPT[$$]" "Starting up"
138 |
139 | # Start the VM
140 | exec $CMD
141 | ;;
142 |
143 | *)
144 | echo "Usage: $SCRIPT {start|stop|restart|reboot|ping|console|attach}"
145 | exit 1
146 | ;;
147 | esac
148 |
149 | exit 0
150 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Chloe
2 | =====
3 |
4 | A realtime web server that doesn't suck... or at least won't suck when it's
5 | finished.
6 |
7 | How it Works
8 | ------------
9 |
10 | The User's browser loads your page [1], which instantiates a connection to
11 | Chloe using JavaScript. It can then send data over that connection [2], which
12 | will be relayed to your app via a POST from Chloe [3]. When you have data that
13 | you want to send back to the browser, send a POST to Chloe [4], which will
14 | relay it back to the connected browser [5].
15 |
16 | 2. Send data over websockets
17 | +---------------------------------------------------------------------------------------------+
18 | | v
19 | +------------------------+ 1. /index.html +----------+ 4. POST /send (data for browser) +-------+
20 | | Browser | ----------------> | Your App | ----------------------------------> | Chloe | -+
21 | +------------------------+ +----------+ +-------+ |
22 | ^ ^ 3. Data from the browser | |
23 | | 5. Data from your app +------------------------------------------------+ |
24 | | |
25 | | |
26 | +------------------------------------------------------------------------------------------------------+
27 |
28 | Support
29 | -------
30 |
31 | Mailing List: `http://groups.google.com/group/chloe-ws`
32 |
33 | Installation
34 | ------------
35 |
36 | Mac Binary
37 | ==========
38 |
39 | curl -LO https://github.com/downloads/mashion/chloe/chloe-0.0.5-osx.tgz
40 | tar xzvf chloe-0.0.5-osx.tgz
41 | cd chloe-0.0.5
42 | ./bin/chloe start
43 |
44 | Ubuntu Binary
45 | =============
46 |
47 | wget https://github.com/downloads/mashion/chloe/chloe-0.0.5-ubuntu-32.tgz
48 | tar xzvf chloe-0.0.5-ubuntu32.tgz
49 | cd chloe-0.0.5
50 | ./bin/chloe start
51 |
52 | From Source
53 | ===========
54 |
55 | - Get you some erlang, on a mac:
56 |
57 | brew install erlang
58 |
59 | - Get rebar: [start here](https://github.com/basho/rebar/wiki/Getting-started). Or for mac:
60 |
61 | brew install rebar
62 |
63 | - Clone this repo:
64 |
65 | git clone https://github.com/mashion/chloe.git
66 | cd chloe
67 |
68 | - Run these commands
69 |
70 | gem install rake
71 | rake bootstrap
72 | rake server
73 |
74 | - get sinatra and run the demo app in another terminal window
75 |
76 | gem install sinatra
77 | rake demo
78 |
79 | - Point your browser at http://localhost:4567 and open up the javascript console
80 | - demo.js sets up a chloe variable that is already connected to the server, do
81 | the following in the console:
82 |
83 | chloe.send("hi mom");
84 |
85 | - Relish the awesome.
86 | - To stop Chloe, enter `q().` back in erlang
87 | - To stop the demo app, use Ctrl+C
88 |
89 | JS API
90 | ------
91 |
92 | Instantiate a Chloe object:
93 |
94 | var chloe = new Chloe({host: 'localhost', port: 8901});
95 |
96 | Define a function for handling incoming messages:
97 |
98 | chloe.onmessage(function (message) {
99 | console.log('I got a message: ' + message);
100 | });
101 |
102 | Connect to Chloe and send a message:
103 |
104 | chloe.connect(function () {
105 | chloe.send('Ohai!');
106 | });
107 |
108 | Subscribe to a channel (note that we do this within the `connect` callback,
109 | because subscribing to a channel requires sending a message to Chloe).
110 |
111 | chloe.connect(function () {
112 | chloe.subscribe('pumpkin', function (message) {
113 | console.log('Someone was eating pumpkins: ' + message);
114 | });
115 | });
116 |
117 | Server Side API
118 | ---------------
119 |
120 | Sending a message to Chloe, which will then be sent to all connected browsers:
121 |
122 | curl -d "data=This is the message data" http://localhost:8901/send
123 |
124 | Sending a message to a specific channel in chloe:
125 |
126 | curl -d "channel=pumpkin&data=Trotter" http://localhost:8901/send
127 |
128 | Configuring Chloe
129 | -----------------
130 |
131 | Chloe has a single configuration file, `chloe-0.0.5/etc/app.config`. Allowed
132 | options for the Chloe application are:
133 |
134 | - **application_server_url**: The url for the application server, ex: http://localhost:4567/updates
135 | - **port**: The port on which chloe runs (default: 8901)
136 | - **log_dir**: Where the Chloe log files will go (default: `.`)
137 |
138 | Transport Types
139 | ---------------
140 |
141 | Chloe currently supports Websockets, XHR, and JSONP as possible transports. If your
142 | browser does not support WebSockets, we'll intelligently fallback to JSONP or XHR.
143 |
144 | Caveats
145 | -------
146 |
147 | Our primary test browser is Chrome. If you're using any other browser, please
148 | let us know if you run into issues.
149 |
150 | This sucker is pretty alpha right now. There's a number of undocumented
151 | features, but everything that is documented seems to work. As with all alpha,
152 | open source software, ymmv. Pull requests gladly accepted!
153 |
--------------------------------------------------------------------------------
/apps/chloe/src/chloe_session.erl:
--------------------------------------------------------------------------------
1 | -module(chloe_session).
2 |
3 | -behaviour(gen_server).
4 | -include_lib("./chloe.hrl").
5 |
6 | %% API
7 | -export([
8 | start_link/1,
9 | send_to_server/2,
10 | subscribe/2,
11 | send_to_browser/3,
12 | retrieve_messages/1,
13 | attach_transport_pid/2
14 | ]).
15 |
16 | %% gen_server callbacks
17 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
18 | terminate/2, code_change/3]).
19 |
20 | -record(state, {transport_pid, messages, failed_health_checks}).
21 |
22 | -define(TIMEOUT, 13 * 1000). % Primes are fun
23 | -define(HEALTH_CHECK_THRESHOLD, 4). % Five fails == death
24 |
25 | %%--------------------------------------------------------------------
26 | %% API functions
27 | %%--------------------------------------------------------------------
28 |
29 | start_link(TransportPid) ->
30 | gen_server:start_link(?MODULE, [TransportPid], []).
31 |
32 | send_to_server(Pid, Data) ->
33 | gen_server:cast(Pid, {send_to_server, Data}).
34 |
35 | subscribe(Pid, Channel) ->
36 | gen_server:cast(Pid, {subscribe, Channel}).
37 |
38 | send_to_browser(Pid, Channel, Data) ->
39 | gen_server:cast(Pid, {send_to_browser, [Channel, Data]}).
40 |
41 | retrieve_messages(Pid) ->
42 | gen_server:call(Pid, retrieve_messages).
43 |
44 | attach_transport_pid(Pid, TransportPid) ->
45 | gen_server:cast(Pid, {attach_transport_pid, TransportPid}).
46 |
47 | %%--------------------------------------------------------------------
48 | %% gen_server callbacks
49 | %%--------------------------------------------------------------------
50 |
51 | init([TransportPid]) ->
52 | chloe_channel_store:subscribe("/all", self()),
53 | {ok, #state{transport_pid = TransportPid, messages = [], failed_health_checks = 0}, ?TIMEOUT}.
54 |
55 | handle_call(retrieve_messages, _From, State) ->
56 | Messages = State#state.messages,
57 | error_logger:info_msg("Old State was ~p~n", [State]),
58 | NewState = State#state{messages= [] },
59 | error_logger:info_msg("New State is ~p~n", [NewState]),
60 | {reply, Messages, NewState, ?TIMEOUT};
61 | handle_call(_Request, _From, State) ->
62 | {reply, ok, State, ?TIMEOUT}.
63 |
64 | handle_cast({send_to_server, Data}, State) ->
65 | send_data_to_server(Data),
66 | {noreply, State, ?TIMEOUT};
67 | handle_cast({subscribe, Channel}, State) ->
68 | error_logger:info_msg("Subscribing to channel ~p~n", [Channel]),
69 | chloe_channel_store:subscribe(Channel, self()),
70 | {noreply, State, ?TIMEOUT};
71 | handle_cast({send_to_browser, [Channel, Data]}, State) ->
72 | %% TODO (trotter): Handle case where we have a pid but
73 | %% it's no longer active.
74 | NewState = case is_transport_available(State#state.transport_pid) of
75 | true -> send_message_to_browser(Channel, Data, State);
76 | _ -> store_message_for_later(Channel, Data, State)
77 | end,
78 | {noreply, NewState, ?TIMEOUT};
79 | handle_cast({attach_transport_pid, TransportPid}, State) ->
80 | Messages = State#state.messages,
81 | case Messages of
82 | [] -> NewState = State;
83 | _ -> NewState = State#state{messages = []},
84 | gen_server:cast(TransportPid, {send, [Messages]})
85 | end,
86 | {noreply, NewState#state{transport_pid = TransportPid}, ?TIMEOUT}.
87 |
88 | handle_info(timeout, State) ->
89 | case check_transport_health(State) of
90 | dead -> {stop, normal, State};
91 | unhealthy -> {noreply,
92 | State#state{failed_health_checks=State#state.failed_health_checks + 1},
93 | ?TIMEOUT};
94 | ok -> {noreply,
95 | State#state{failed_health_checks=0},
96 | ?TIMEOUT}
97 | end;
98 | %% This will come to us as the response to our request
99 | %% to the app server. We don't care what the app server
100 | %% said.
101 | handle_info({http, _}, State) ->
102 | {noreply, State, ?TIMEOUT}.
103 |
104 | terminate(_Reason, _State) ->
105 | ok.
106 |
107 | code_change(_OldVsn, State, _Extra) ->
108 | {ok, State}.
109 |
110 | %%--------------------------------------------------------------------
111 | %% Internal Functions
112 | %%--------------------------------------------------------------------
113 |
114 | check_transport_health(State) ->
115 | case is_transport_available(State#state.transport_pid) of
116 | true -> ok;
117 | _ -> case State#state.failed_health_checks > ?HEALTH_CHECK_THRESHOLD of
118 | true -> dead;
119 | _ -> unhealthy
120 | end
121 | end.
122 |
123 | send_data_to_server(Data) ->
124 | {ok, Url} = application:get_env(chloe, application_server_url),
125 | {ok, Signature} = create_signature(Data),
126 | httpc:request(post, {Url,
127 | [{"x-chloe-signature", Signature}],
128 | "text/plain",
129 | Data},
130 | [], [{sync, false}]).
131 |
132 | create_signature(Data) ->
133 | case application:get_env(chloe, secret) of
134 | undefined -> {ok, ""};
135 | {ok, Secret} -> {ok, lib_md5:hexdigest(Data ++ Secret)}
136 | end.
137 |
138 | store_message_for_later(Channel, Data, State) ->
139 | Messages = [#message{channel=Channel, data=Data} | State#state.messages],
140 | State#state{messages=Messages, transport_pid=undefined}.
141 |
142 | send_message_to_browser(Channel, Data, State) ->
143 | Messages = [#message{channel=Channel, data=Data}],
144 | gen_server:cast(State#state.transport_pid, {send, [Messages]}),
145 | State.
146 |
147 | is_transport_available(TransportPid) ->
148 | case TransportPid of
149 | undefined -> false;
150 | _ -> is_process_alive(TransportPid)
151 | end.
152 |
--------------------------------------------------------------------------------
/javascripts/json2.js:
--------------------------------------------------------------------------------
1 | // Create a JSON object only if one does not already exist. We create the
2 | // methods in a closure to avoid creating global variables.
3 |
4 | var JSON;
5 | if (!JSON) {
6 | JSON = {};
7 | }
8 |
9 | (function () {
10 | "use strict";
11 |
12 | function f(n) {
13 | // Format integers to have at least two digits.
14 | return n < 10 ? '0' + n : n;
15 | }
16 |
17 | if (typeof Date.prototype.toJSON !== 'function') {
18 |
19 | Date.prototype.toJSON = function (key) {
20 |
21 | return isFinite(this.valueOf()) ?
22 | this.getUTCFullYear() + '-' +
23 | f(this.getUTCMonth() + 1) + '-' +
24 | f(this.getUTCDate()) + 'T' +
25 | f(this.getUTCHours()) + ':' +
26 | f(this.getUTCMinutes()) + ':' +
27 | f(this.getUTCSeconds()) + 'Z' : null;
28 | };
29 |
30 | String.prototype.toJSON =
31 | Number.prototype.toJSON =
32 | Boolean.prototype.toJSON = function (key) {
33 | return this.valueOf();
34 | };
35 | }
36 |
37 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
38 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
39 | gap,
40 | indent,
41 | meta = { // table of character substitutions
42 | '\b': '\\b',
43 | '\t': '\\t',
44 | '\n': '\\n',
45 | '\f': '\\f',
46 | '\r': '\\r',
47 | '"' : '\\"',
48 | '\\': '\\\\'
49 | },
50 | rep;
51 |
52 |
53 | function quote(string) {
54 |
55 | // If the string contains no control characters, no quote characters, and no
56 | // backslash characters, then we can safely slap some quotes around it.
57 | // Otherwise we must also replace the offending characters with safe escape
58 | // sequences.
59 |
60 | escapable.lastIndex = 0;
61 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
62 | var c = meta[a];
63 | return typeof c === 'string' ? c :
64 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
65 | }) + '"' : '"' + string + '"';
66 | }
67 |
68 |
69 | function str(key, holder) {
70 |
71 | // Produce a string from holder[key].
72 |
73 | var i, // The loop counter.
74 | k, // The member key.
75 | v, // The member value.
76 | length,
77 | mind = gap,
78 | partial,
79 | value = holder[key];
80 |
81 | // If the value has a toJSON method, call it to obtain a replacement value.
82 |
83 | if (value && typeof value === 'object' &&
84 | typeof value.toJSON === 'function') {
85 | value = value.toJSON(key);
86 | }
87 |
88 | // If we were called with a replacer function, then call the replacer to
89 | // obtain a replacement value.
90 |
91 | if (typeof rep === 'function') {
92 | value = rep.call(holder, key, value);
93 | }
94 |
95 | // What happens next depends on the value's type.
96 |
97 | switch (typeof value) {
98 | case 'string':
99 | return quote(value);
100 |
101 | case 'number':
102 |
103 | // JSON numbers must be finite. Encode non-finite numbers as null.
104 |
105 | return isFinite(value) ? String(value) : 'null';
106 |
107 | case 'boolean':
108 | case 'null':
109 |
110 | // If the value is a boolean or null, convert it to a string. Note:
111 | // typeof null does not produce 'null'. The case is included here in
112 | // the remote chance that this gets fixed someday.
113 |
114 | return String(value);
115 |
116 | // If the type is 'object', we might be dealing with an object or an array or
117 | // null.
118 |
119 | case 'object':
120 |
121 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
122 | // so watch out for that case.
123 |
124 | if (!value) {
125 | return 'null';
126 | }
127 |
128 | // Make an array to hold the partial results of stringifying this object value.
129 |
130 | gap += indent;
131 | partial = [];
132 |
133 | // Is the value an array?
134 |
135 | if (Object.prototype.toString.apply(value) === '[object Array]') {
136 |
137 | // The value is an array. Stringify every element. Use null as a placeholder
138 | // for non-JSON values.
139 |
140 | length = value.length;
141 | for (i = 0; i < length; i += 1) {
142 | partial[i] = str(i, value) || 'null';
143 | }
144 |
145 | // Join all of the elements together, separated with commas, and wrap them in
146 | // brackets.
147 |
148 | v = partial.length === 0 ? '[]' : gap ?
149 | '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
150 | '[' + partial.join(',') + ']';
151 | gap = mind;
152 | return v;
153 | }
154 |
155 | // If the replacer is an array, use it to select the members to be stringified.
156 |
157 | if (rep && typeof rep === 'object') {
158 | length = rep.length;
159 | for (i = 0; i < length; i += 1) {
160 | if (typeof rep[i] === 'string') {
161 | k = rep[i];
162 | v = str(k, value);
163 | if (v) {
164 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
165 | }
166 | }
167 | }
168 | } else {
169 |
170 | // Otherwise, iterate through all of the keys in the object.
171 |
172 | for (k in value) {
173 | if (Object.prototype.hasOwnProperty.call(value, k)) {
174 | v = str(k, value);
175 | if (v) {
176 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
177 | }
178 | }
179 | }
180 | }
181 |
182 | // Join all of the member texts together, separated with commas,
183 | // and wrap them in braces.
184 |
185 | v = partial.length === 0 ? '{}' : gap ?
186 | '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
187 | '{' + partial.join(',') + '}';
188 | gap = mind;
189 | return v;
190 | }
191 | }
192 |
193 | // If the JSON object does not yet have a stringify method, give it one.
194 |
195 | if (typeof JSON.stringify !== 'function') {
196 | JSON.stringify = function (value, replacer, space) {
197 |
198 | // The stringify method takes a value and an optional replacer, and an optional
199 | // space parameter, and returns a JSON text. The replacer can be a function
200 | // that can replace values, or an array of strings that will select the keys.
201 | // A default replacer method can be provided. Use of the space parameter can
202 | // produce text that is more easily readable.
203 |
204 | var i;
205 | gap = '';
206 | indent = '';
207 |
208 | // If the space parameter is a number, make an indent string containing that
209 | // many spaces.
210 |
211 | if (typeof space === 'number') {
212 | for (i = 0; i < space; i += 1) {
213 | indent += ' ';
214 | }
215 |
216 | // If the space parameter is a string, it will be used as the indent string.
217 |
218 | } else if (typeof space === 'string') {
219 | indent = space;
220 | }
221 |
222 | // If there is a replacer, it must be a function or an array.
223 | // Otherwise, throw an error.
224 |
225 | rep = replacer;
226 | if (replacer && typeof replacer !== 'function' &&
227 | (typeof replacer !== 'object' ||
228 | typeof replacer.length !== 'number')) {
229 | throw new Error('JSON.stringify');
230 | }
231 |
232 | // Make a fake root object containing our value under the key of ''.
233 | // Return the result of stringifying the value.
234 |
235 | return str('', {'': value});
236 | };
237 | }
238 |
239 |
240 | // If the JSON object does not yet have a parse method, give it one.
241 |
242 | if (typeof JSON.parse !== 'function') {
243 | JSON.parse = function (text, reviver) {
244 |
245 | // The parse method takes a text and an optional reviver function, and returns
246 | // a JavaScript value if the text is a valid JSON text.
247 |
248 | var j;
249 |
250 | function walk(holder, key) {
251 |
252 | // The walk method is used to recursively walk the resulting structure so
253 | // that modifications can be made.
254 |
255 | var k, v, value = holder[key];
256 | if (value && typeof value === 'object') {
257 | for (k in value) {
258 | if (Object.prototype.hasOwnProperty.call(value, k)) {
259 | v = walk(value, k);
260 | if (v !== undefined) {
261 | value[k] = v;
262 | } else {
263 | delete value[k];
264 | }
265 | }
266 | }
267 | }
268 | return reviver.call(holder, key, value);
269 | }
270 |
271 |
272 | // Parsing happens in four stages. In the first stage, we replace certain
273 | // Unicode characters with escape sequences. JavaScript handles many characters
274 | // incorrectly, either silently deleting them, or treating them as line endings.
275 |
276 | text = String(text);
277 | cx.lastIndex = 0;
278 | if (cx.test(text)) {
279 | text = text.replace(cx, function (a) {
280 | return '\\u' +
281 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
282 | });
283 | }
284 |
285 | // In the second stage, we run the text against regular expressions that look
286 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
287 | // because they can cause invocation, and '=' because it can cause mutation.
288 | // But just to be safe, we want to reject all unexpected forms.
289 |
290 | // We split the second stage into 4 regexp operations in order to work around
291 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
292 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
293 | // replace all simple value tokens with ']' characters. Third, we delete all
294 | // open brackets that follow a colon or comma or that begin the text. Finally,
295 | // we look to see that the remaining characters are only whitespace or ']' or
296 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
297 |
298 | if (/^[\],:{}\s]*$/
299 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
300 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
301 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
302 |
303 | // In the third stage we use the eval function to compile the text into a
304 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
305 | // in JavaScript: it can begin a block or an object literal. We wrap the text
306 | // in parens to eliminate the ambiguity.
307 |
308 | j = eval('(' + text + ')');
309 |
310 | // In the optional fourth stage, we recursively walk the new structure, passing
311 | // each name/value pair to a reviver function for possible transformation.
312 |
313 | return typeof reviver === 'function' ?
314 | walk({'': j}, '') : j;
315 | }
316 |
317 | // If the text is not JSON parseable, then a SyntaxError is thrown.
318 |
319 | throw new SyntaxError('JSON.parse');
320 | };
321 | }
322 | }());
323 |
--------------------------------------------------------------------------------
/apps/chloe/test/gen_server_mock.erl:
--------------------------------------------------------------------------------
1 |
2 | %%%-------------------------------------------------------------------
3 | %%% File : gen_server_mock.erl
4 | %%% Author : nmurray@attinteractive.com
5 | %%% Description : Mocking for gen_server. Expectations are ordered, every
6 | %%% message required and no messages more than are expected are allowed.
7 | %%%
8 | %%% Expectations get the same input as the handle_(whatever) gen_server methods. They should return -> ok | {ok, NewState}
9 | %%% Created : 2009-08-05
10 | %%% Inspired by: http://erlang.org/pipermail/erlang-questions/2008-April/034140.html
11 | %%%-------------------------------------------------------------------
12 |
13 | -module(gen_server_mock).
14 | -behaviour(gen_server).
15 |
16 | % API
17 | -export([new/0, new/1, stop/1, crash/1,
18 | expect/3, expect_call/2, expect_info/2, expect_cast/2,
19 | assert_expectations/1]).
20 |
21 | % gen_server callbacks
22 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
23 | code_change/3]).
24 |
25 | %% Macros
26 | -define(SERVER, ?MODULE).
27 | -define(DEFAULT_CONFIG, {}).
28 |
29 | -record(state, {
30 | expectations
31 | }).
32 |
33 | -record(expectation, {
34 | type,
35 | lambda
36 | }).
37 |
38 | % steal assert from eunit
39 | -define(assert(BoolExpr),
40 | ((fun () ->
41 | case (BoolExpr) of
42 | true -> ok;
43 | __V -> .erlang:error({assertion_failed,
44 | [{module, ?MODULE},
45 | {line, ?LINE},
46 | {expression, (??BoolExpr)},
47 | {expected, true},
48 | {value, case __V of false -> __V;
49 | _ -> {not_a_boolean,__V}
50 | end}]})
51 | end
52 | end)())).
53 |
54 | -define(raise(ErrorName),
55 | erlang:error({ErrorName,
56 | [{module, ?MODULE},
57 | {line, ?LINE}]})).
58 |
59 | -define(raise_info(ErrorName, Info),
60 | erlang:error({ErrorName,
61 | [{module, ?MODULE},
62 | {line, ?LINE},
63 | {info, Info}
64 | ]})).
65 |
66 |
67 | -define (DEBUG, true).
68 | -define (TRACE(X, M), case ?DEBUG of
69 | true -> io:format(user, "TRACE ~p:~p ~p ~p~n", [?MODULE, ?LINE, X, M]);
70 | false -> ok
71 | end).
72 |
73 | %%====================================================================
74 | %% API
75 | %%====================================================================
76 | %%--------------------------------------------------------------------
77 | %% Function: start() -> {ok,Pid} | ignore | {error,Error}
78 | %% Description: Alias for start_link
79 | %%--------------------------------------------------------------------
80 | start() ->
81 | start_link([]).
82 |
83 | %%--------------------------------------------------------------------
84 | %% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
85 | %% Description: Starts the server
86 | %%--------------------------------------------------------------------
87 | start_link(Config) ->
88 | gen_server:start_link(?MODULE, [Config], []). % start a nameless server
89 |
90 | %%--------------------------------------------------------------------
91 | %% Function: new() -> {ok, Mock} | {error, Error}
92 | %% Description:
93 | %%--------------------------------------------------------------------
94 | new() ->
95 | case start() of
96 | {ok, Pid} ->
97 | {ok, Pid};
98 | {error, Error} ->
99 | {error, Error};
100 | Other ->
101 | {error, Other}
102 | end.
103 |
104 | %%--------------------------------------------------------------------
105 | %% Function: new(N) when is_integer(N) -> [Pids]
106 | %% Description: Return multiple Mock gen_servers
107 | %%--------------------------------------------------------------------
108 | new(N) when is_integer(N) -> % list() of Pids
109 | lists:map(fun(_) -> {ok, Mock} = new(), Mock end, lists:seq(1, N)).
110 |
111 | %%--------------------------------------------------------------------
112 | %% Function: expect(Mock, Type, Callback) -> ok
113 | %% Types: Mock = pid()
114 | %% Type = atom() = call | cast | info
115 | %% Callback = fun(Args) -> ok | {ok, NewState} | {ok, ResponseValue, NewState}
116 | %% Args matches signature of handle_* in gen_server. e.g. handle_call
117 | %%
118 | %% Description: Set an expectation of Type
119 | %%--------------------------------------------------------------------
120 | expect(Mock, Type, Callback) ->
121 | Exp = #expectation{type=Type, lambda=Callback},
122 | added = gen_server:call(Mock, {expect, Exp}),
123 | ok.
124 |
125 | %%--------------------------------------------------------------------
126 | %% Function: expect_call(Mock, Callback) -> ok
127 | %% Types: Mock = pid()
128 | %% Callback = fun(Args) -> ok | {ok, NewState} | {ok, ResponseValue, NewState}
129 | %% Args matches signature of handle_call in gen_server.
130 | %%
131 | %% Description: Set a call expectation
132 | %%--------------------------------------------------------------------
133 | expect_call(Mock, Callback) ->
134 | expect(Mock, call, Callback).
135 |
136 | %%--------------------------------------------------------------------
137 | %% Function: expect_info(Mock, Callback) -> ok
138 | %% Types: Mock = pid()
139 | %% Callback = fun(Args) -> ok | {ok, NewState} | {ok, ResponseValue, NewState}
140 | %% Args matches signature of handle_info in gen_server.
141 | %%
142 | %% Description: Set a info expectation
143 | %%--------------------------------------------------------------------
144 | expect_info(Mock, Callback) ->
145 | expect(Mock, info, Callback).
146 |
147 | %%--------------------------------------------------------------------
148 | %% Function: expect_cast(Mock, Callback) -> ok
149 | %% Types: Mock = pid()
150 | %% Callback = fun(Args) -> ok | {ok, NewState} | {ok, ResponseValue, NewState}
151 | %% Args matches signature of handle_cast in gen_server.
152 | %%
153 | %% Description: Set a cast expectation
154 | %%--------------------------------------------------------------------
155 | expect_cast(Mock, Callback) ->
156 | expect(Mock, cast, Callback).
157 |
158 | %%--------------------------------------------------------------------
159 | %% Function: assert_expectations(Mock)-> ok
160 | %% Types: Mock = pid() | [Mocks]
161 | %% Description: Ensure expectations were fully met
162 | %%--------------------------------------------------------------------
163 | assert_expectations(Mock) when is_pid(Mock) ->
164 | assert_expectations([Mock]);
165 | assert_expectations([H|T]) ->
166 | timer:sleep(1), % This sleep(1) seems to give the other processes time
167 | % to send their requests through.
168 | gen_server:call(H, assert_expectations),
169 | ok = assert_expectations(T);
170 | assert_expectations([]) ->
171 | ok.
172 |
173 | %%--------------------------------------------------------------------
174 | %% Function: stop(Mock)-> ok
175 | %% Types: Mock = pid() | [Mocks]
176 | %% Description: Stop the Mock gen_server normally
177 | %%--------------------------------------------------------------------
178 | stop(H) when is_pid(H) ->
179 | stop([H]);
180 | stop([H|T]) ->
181 | gen_server:cast(H, {'$gen_server_mock', stop}),
182 | stop(T);
183 | stop([]) ->
184 | ok.
185 |
186 | crash(H) when is_pid(H) ->
187 | crash([H]);
188 | crash([H|T]) ->
189 | gen_server:cast(H, {'$gen_server_mock', crash}),
190 | crash(T);
191 | crash([]) ->
192 | ok.
193 |
194 |
195 |
196 | %%====================================================================
197 | %% gen_server callbacks
198 | %%====================================================================
199 |
200 | %%--------------------------------------------------------------------
201 | %% Function: init(Args) -> {ok, State} |
202 | %% {ok, State, Timeout} |
203 | %% ignore |
204 | %% {stop, Reason}
205 | %% Description: Initiates the server
206 | %%--------------------------------------------------------------------
207 |
208 | init(_Args) ->
209 | InitialState = #state{expectations=[]},
210 | {ok, InitialState}.
211 |
212 | %%--------------------------------------------------------------------
213 | %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
214 | %% {reply, Reply, State, Timeout} |
215 | %% {noreply, State} |
216 | %% {noreply, State, Timeout} |
217 | %% {stop, Reason, Reply, State} |
218 | %% {stop, Reason, State}
219 | %% Description: Handling call messages
220 | %%--------------------------------------------------------------------
221 |
222 | % return the state
223 | handle_call(state, _From, State) ->
224 | {reply, {ok, State}, State};
225 |
226 | handle_call({expect, Expectation}, _From, State) ->
227 | {ok, NewState} = store_expectation(Expectation, State),
228 | {reply, added, NewState};
229 |
230 | handle_call(assert_expectations, _From, State) ->
231 | {ok, NewState} = handle_assert_expectations(State),
232 | {reply, ok, NewState};
233 |
234 | handle_call(Request, From, State) ->
235 | {ok, Reply, NewState} = reply_with_next_expectation(call, Request, From, undef, undef, State),
236 | {reply, Reply, NewState}.
237 |
238 | %%--------------------------------------------------------------------
239 | %% Function: handle_cast(Msg, State) -> {noreply, State} |
240 | %% {noreply, State, Timeout} |
241 | %% {stop, Reason, State}
242 | %% Description: Handling cast messages
243 | %%--------------------------------------------------------------------
244 | handle_cast({'$gen_server_mock', stop}, State) ->
245 | {stop, normal, State};
246 | handle_cast({'$gen_server_mock', crash}, State) ->
247 | {stop, crash, State};
248 | handle_cast(Msg, State) ->
249 | {ok, _Reply, NewState} = reply_with_next_expectation(cast, undef, undef, Msg, undef, State),
250 | {noreply, NewState}.
251 |
252 | %%--------------------------------------------------------------------
253 | %% Function: handle_info(Info, State) -> {noreply, State} |
254 | %% {noreply, State, Timeout} |
255 | %% {stop, Reason, State}
256 | %% Description: Handling all non call/cast messages
257 | %%--------------------------------------------------------------------
258 | handle_info(Info, State) ->
259 | {ok, _Reply, NewState} = reply_with_next_expectation(info, undef, undef, undef, Info, State),
260 | {noreply, NewState}.
261 |
262 | %%--------------------------------------------------------------------
263 | %% Function: terminate(Reason, State) -> void()
264 | %% Description: This function is called by a gen_server when it is about to
265 | %% terminate. It should be the opposite of Module:init/1 and do any necessary
266 | %% cleaning up. When it returns, the gen_server terminates with Reason.
267 | %% The return value is ignored.
268 | %%--------------------------------------------------------------------
269 | terminate(_Reason, _State) ->
270 | ok.
271 |
272 | %%--------------------------------------------------------------------
273 | %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
274 | %% Description: Convert process state when code is changed
275 | %%--------------------------------------------------------------------
276 | code_change(_OldVsn, State, _Extra) ->
277 | {ok, State}.
278 |
279 | %%
280 | %% private functions
281 | %%
282 | store_expectation(Expectation, State) -> % {ok, NewState}
283 | NewExpectations = [Expectation|State#state.expectations],
284 | NewState = State#state{expectations = NewExpectations},
285 | {ok, NewState}.
286 |
287 | pop(L) -> % {Result, NewList} | {undef, []}
288 | case L of
289 | [] -> {undef, []};
290 | List -> {lists:last(List), lists:sublist(List, 1, length(List) - 1)}
291 | end.
292 |
293 | pop_expectation(State) -> % {ok, Expectation, NewState}
294 | {Expectation, RestExpectations} = case pop(State#state.expectations) of
295 | {undef, []} -> ?raise(no_gen_server_mock_expectation);
296 | {Head, Rest} -> {Head, Rest}
297 | end,
298 | NewState = State#state{expectations = RestExpectations},
299 | {ok, Expectation, NewState}.
300 |
301 | handle_assert_expectations(State) -> % {ok, State}
302 | ExpLeft = State#state.expectations,
303 | case length(ExpLeft) > 0 of
304 | true -> ?raise_info(unmet_gen_server_expectation, ExpLeft);
305 | false -> ok
306 | end,
307 | {ok, State}.
308 |
309 | reply_with_next_expectation(Type, Request, From, Msg, Info, State) -> % -> {ok, Reply, NewState}
310 | {ok, Expectation, NewState} = pop_expectation(State),
311 | ?assert(Type =:= Expectation#expectation.type), % todo, have a useful error message, "expected this got that"
312 |
313 | {ok, Reply, NewState2} = try call_expectation_lambda(Expectation, Type, Request, From, Msg, Info, NewState) of
314 | {ok, R, State2} -> {ok, R, State2}
315 | catch
316 | error:function_clause ->
317 | ?raise_info(unexpected_request_made, {Expectation, Type, Request, From, Msg, Info, NewState})
318 | end,
319 | {ok, Reply, NewState2}.
320 |
321 | % hmm what if we want better response.
322 | call_expectation_lambda(Expectation, Type, Request, From, Msg, Info, State) -> % {ok, NewState}
323 | L = Expectation#expectation.lambda,
324 | Response = case Type of
325 | call -> L(Request, From, State);
326 | cast -> L(Msg, State);
327 | info -> L(Info, State);
328 | _ -> L(Request, From, Msg, Info, State)
329 | end,
330 | case Response of % hmmm
331 | ok -> {ok, ok, State};
332 | {ok, NewState} -> {ok, ok, NewState};
333 | {ok, ResponseValue, NewState} -> {ok, ResponseValue, NewState};
334 | Other -> Other
335 | end.
336 |
--------------------------------------------------------------------------------