├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── doc └── assets │ └── west_arch_layer_view.png ├── ebin └── .gitignore ├── include └── west.hrl ├── priv └── scripts │ └── remove_deps.script ├── rebar ├── rebar.config ├── rebar.config.script ├── rel ├── files │ ├── app.config │ ├── erl │ ├── nodetool │ ├── sys.config │ ├── vm.args │ ├── west │ └── west-admin ├── reltool.config ├── vars.config └── vars │ ├── dev1.config │ ├── dev2.config │ └── dev3.config ├── src ├── endpoints │ ├── cowboy │ │ ├── west_cowboy_ws_json_handler.erl │ │ ├── west_cowboy_ws_pb_handler.erl │ │ └── west_cowboy_ws_text_handler.erl │ └── yaws │ │ ├── west_yaws_ws_endpoint.erl │ │ ├── west_yaws_ws_json_handler.erl │ │ ├── west_yaws_ws_pb_handler.erl │ │ └── west_yaws_ws_text_handler.erl ├── protos │ └── message.proto ├── west.app.src ├── west.erl ├── west_app.erl ├── west_dist.erl ├── west_dist_cmd_fsm.erl ├── west_dist_cmd_fsm_sup.erl ├── west_dist_console.erl ├── west_dist_vnode.erl ├── west_event_handler.erl ├── west_event_handler_sup.erl ├── west_int.hrl ├── west_lib.erl ├── west_msg.erl ├── west_protocol.hrl ├── west_protocol_handler.erl ├── west_sup.erl └── west_util.erl ├── test └── west_tests.erl └── www ├── assets ├── css │ ├── docs.css │ └── pygments-manni.css ├── files │ └── west_msg.proto ├── ico │ ├── apple-touch-icon-114-precomposed.png │ ├── apple-touch-icon-144-precomposed.png │ ├── apple-touch-icon-57-precomposed.png │ ├── apple-touch-icon-72-precomposed.png │ └── favicon.png └── js │ ├── ByteBuffer.min.js │ ├── Long.min.js │ ├── ProtoBuf.min.js │ ├── application.js │ ├── customizer.js │ ├── filesaver.js │ ├── holder.js │ ├── html5shiv.js │ ├── jquery.js │ ├── jszip.js │ ├── less.js │ ├── raw-files.js │ ├── respond.min.js │ └── uglify.js ├── bootstrap ├── css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap.css │ ├── bootstrap.css.map │ └── bootstrap.min.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff └── js │ ├── bootstrap.js │ └── bootstrap.min.js ├── css ├── my_theme.css └── starter-template.css ├── index.html └── int ├── jsonwp.html ├── protobuffs.html └── textwp.html /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | .idea 8 | .DS_Store 9 | ._* 10 | *~ 11 | *.iml 12 | priv/data/* 13 | rel/west 14 | dev 15 | log 16 | data 17 | *_server* 18 | .rebar 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all deps compile clean eunit test rel 2 | 3 | all: deps compile 4 | 5 | compile: 6 | ./rebar compile 7 | 8 | deps: 9 | ./rebar get-deps 10 | 11 | clean: 12 | ./rebar clean 13 | 14 | eunit: 15 | ./rebar skip_deps=true compile eunit 16 | 17 | test: eunit 18 | 19 | distclean: clean devclean relclean 20 | ./rebar delete-deps 21 | 22 | rel: all 23 | ./rebar skip_deps=true generate 24 | 25 | relclean: 26 | rm -rf rel/west 27 | 28 | devrel: dev1 dev2 dev3 29 | 30 | devclean: 31 | rm -rf dev 32 | 33 | dev1 dev2 dev3: 34 | mkdir -p dev 35 | (cd rel && ../rebar generate target_dir=../dev/$@ overlay_vars=vars/$@.config) 36 | 37 | clean_all: 38 | rm -rf rel/west 39 | rm -rf dev 40 | ./rebar clean 41 | -------------------------------------------------------------------------------- /doc/assets/west_arch_layer_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/doc/assets/west_arch_layer_view.png -------------------------------------------------------------------------------- /ebin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | 4 | -------------------------------------------------------------------------------- /include/west.hrl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Common definitions. 25 | %%% @end 26 | %%% Created : 03. Oct 2013 9:57 AM 27 | %%%------------------------------------------------------------------- 28 | 29 | -record(callback_t, {mod, func, args}). 30 | -define(CALLBACK, #callback_t). 31 | 32 | -record(msg_t, {id, from, event, channel, data}). 33 | -define(MSG, #msg_t). 34 | 35 | -define(MSG_FIELDS, [id, from, event, channel, data]). 36 | 37 | -record(channel_t, {name, type, user_id, date}). 38 | -define(CHANNEL, #channel_t). 39 | 40 | -record(west_t, {name, key, dist, dist_props, scope, cb, encoding}). 41 | -define(WEST, #west_t). 42 | 43 | -type scope() :: l | g. 44 | -type event() :: any(). 45 | -type msg() :: any(). 46 | -type key() :: iolist() | atom(). 47 | -type bucket() :: iolist() | atom(). 48 | -type cb_spec() :: {module(), function() | atom(), [term()] | undefined}. 49 | -type msg_spec() :: #msg_t{}. 50 | -type name() :: atom(). 51 | -type gname() :: term(). 52 | -type server_ref() :: name() | {name(), node()} | {global, gname()} | pid(). 53 | -type property() :: {atom(), term()}. 54 | -type proplist() :: [property()]. 55 | 56 | -define(PRINT(Var), 57 | io:format("DEBUG: ~p:~p - ~p~n~n ~p~n~n", [?MODULE, ?LINE, ??Var, Var]) 58 | ). 59 | 60 | -define(LOG_INFO(Format, Vars), 61 | error_logger:info_msg(Format, Vars) 62 | ). 63 | -define(LOG_ERROR(Format, Vars), 64 | error_logger:error_msg(Format, Vars) 65 | ). 66 | -define(LOG_WARNING(Format, Vars), 67 | error_logger:warning_msg(Format, Vars) 68 | ). 69 | 70 | -define(GPROC_SCOPE(X), 71 | case X of 72 | gproc_dist -> g; 73 | _ -> l 74 | end 75 | ). 76 | 77 | -define(N, 3). 78 | -define(R, 2). 79 | -define(W, 2). 80 | 81 | %% This macro will create a function that converts a record to 82 | %% a {key, value} list (a proplist) 83 | %% Taken from 84 | -define(record_to_list(Record), 85 | fun(Val) -> 86 | Fields = record_info(fields, Record), 87 | [_Tag | Values] = tuple_to_list(Val), 88 | lists:zip(Fields, Values) 89 | end 90 | ). 91 | -------------------------------------------------------------------------------- /priv/scripts/remove_deps.script: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | %% 3 | %% Assumes the following bound variables: 4 | %% CONFIG - a rebar.config options list 5 | %% DEPS :: [atom()] - a list of deps to remove 6 | case lists:keyfind(deps, 1, CONFIG) of 7 | {_, Deps0} -> 8 | Deps1 = lists:filter( 9 | fun(D) when is_atom(D) -> 10 | not lists:member(D, DEPS); 11 | (D) when is_tuple(D) -> 12 | not lists:member(element(1,D), DEPS) 13 | end, Deps0), 14 | lists:keyreplace(deps, 1, CONFIG, {deps, Deps1}); 15 | false -> 16 | CONFIG 17 | end. 18 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, fail_on_warning]}. 2 | 3 | {sub_dirs, ["rel"]}. 4 | 5 | {cover_enabled, true}. 6 | 7 | {deps, [ 8 | {gproc, ".*", {git, "https://github.com/uwiger/gproc.git", {branch, "master"}}}, 9 | {jiffy, ".*", {git, "https://github.com/davisp/jiffy.git", {branch, "master"}}}, 10 | {cowboy, ".*", {git, "https://github.com/extend/cowboy.git", {tag, "1.0.1"}}}, 11 | {protobuffs, ".*", {git, "https://github.com/basho/erlang_protobuffs.git", {branch, "develop"}}}, 12 | {riak_core, ".*", {git, "git://github.com/basho/riak_core", {branch, "develop"}}}, 13 | {yaws, ".*", {git, "https://github.com/klacke/yaws.git", {tag, "yaws-1.98"}}} 14 | ] 15 | }. 16 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | Script = fun(D,S,Vs) -> 3 | Scr = filename:join(D, S), 4 | case file:script(Scr, orddict:store('SCRIPT', Scr, Vs)) of 5 | {ok, Res} -> Res; 6 | {error,_} = Err -> 7 | io:fwrite("Error evaluating script ~s~n", [S]), 8 | Err 9 | end 10 | end. 11 | CFG1 = case os:getenv("REBAR_DEPS") of 12 | false -> CONFIG; 13 | [] -> CONFIG; 14 | Dir -> lists:keystore(deps_dir, 1, CONFIG, {deps_dir, Dir}) 15 | end. 16 | Priv = filename:join(filename:dirname(SCRIPT), "priv/scripts"). 17 | CFG2 = case os:getenv("WEST_DIST") of 18 | "true" -> 19 | CFG1; 20 | F when F=="false"; F==false -> 21 | Script(Priv, 22 | "remove_deps.script", 23 | [{'CONFIG', CFG1}, {'DEPS', [riak_core]}]) 24 | end. 25 | CFG3 = case os:getenv("WEST_YAWS") of 26 | "true" -> 27 | CFG2; 28 | F when F=="false"; F==false -> 29 | Script(Priv, 30 | "remove_deps.script", 31 | [{'CONFIG', CFG2}, {'DEPS', [yaws]}]) 32 | end. 33 | CFG4 = case os:getenv("WEST_COWBOY") of 34 | "false" -> 35 | Script(Priv, 36 | "remove_deps.script", 37 | [{'CONFIG', CFG3}, {'DEPS', [cowboy]}]); 38 | F when F=="true"; F==false -> 39 | CFG3 40 | end. 41 | CFG5 = case os:getenv("WEST_ALL") of 42 | "true" -> 43 | CFG1; 44 | F when F=="false"; F==false -> 45 | CFG4 46 | end. 47 | -------------------------------------------------------------------------------- /rel/files/app.config: -------------------------------------------------------------------------------- 1 | [ 2 | %% Riak Core config 3 | {riak_core, [ 4 | %% Default location of ringstate 5 | {ring_state_dir, "{{ring_state_dir}}"}, 6 | 7 | %% http is a list of IP addresses and TCP ports that the Riak 8 | %% HTTP interface will bind. 9 | {http, [ { "{{web_ip}}", {{web_port}} } ] }, 10 | 11 | %% riak_handoff_port is the TCP port that Riak uses for 12 | %% intra-cluster data handoff. 13 | {handoff_port, {{handoff_port}} } 14 | ] 15 | }, 16 | 17 | %% SASL config 18 | {sasl, [ 19 | {sasl_error_logger, {file, "log/sasl-error.log"}}, 20 | {errlog_type, error}, 21 | {error_logger_mf_dir, "log/sasl"}, % Log directory 22 | {error_logger_mf_maxbytes, 10485760}, % 10 MB max file size 23 | {error_logger_mf_maxfiles, 5} % 5 files max 24 | ] 25 | }, 26 | 27 | %% YAWS config 28 | %% @see YAWS Docs 29 | {yaws, [ 30 | {docroot, "{{server_docroot}}" }, 31 | {gconf, [{id, "embedded"}, 32 | {ebin_dir, ["{{server_ebin_dir}}"]}, 33 | {runmod, "yapp"}]}, 34 | {sconf, [{servername, "{{server_name}}" }, 35 | {listen, {{server_ip}} }, 36 | {port, {{server_port}} }, 37 | {docroot, "{{server_docroot}}" }, 38 | {appmods, [{"websocket", west_yaws_ws_endpoint}]}, 39 | {opaque, [{yapp_server_id, "yapp_west"}, 40 | {bootstrap_yapps, "west"}]}]} 41 | ] 42 | }, 43 | 44 | %% Cowboy 45 | %% @see Cowboy Docs 46 | {cowboy, [ 47 | {routes, [ 48 | {'_', [ 49 | {"/", cowboy_static, {file, "{{server_docroot}}/index.html"}}, 50 | {"{{cowboy_ws_resource}}/text/:key", west_cowboy_ws_text_handler, []}, 51 | {"{{cowboy_ws_resource}}/json/:key", west_cowboy_ws_json_handler, []}, 52 | {"{{cowboy_ws_resource}}/pb/:key", west_cowboy_ws_pb_handler, []}, 53 | {"/[...]", cowboy_static, {dir, "{{server_docroot}}", [{mimetypes, cow_mimetypes, all}]}} 54 | ] 55 | } 56 | ] 57 | }, 58 | {trans_opts, [{port, {{server_port}} }]}, 59 | {c_acceptors, {{cowboy_c_acceptors}} } 60 | ] 61 | }, 62 | 63 | %% WEST 64 | {west, [ 65 | %% WEST distribution model. 66 | %% west_dist: Riak Core + Gproc local 67 | %% gproc_dist: Gproc distributed (gen_leader) 68 | %% gproc: Gproc local 69 | {dist, {{dist}} }, 70 | 71 | %% dist_props opts. 72 | %% dist_props_opts_n = replicas 73 | %% dist_props_opts_q = quorum 74 | {dist_props, [{opts, [{n, {{dist_props_opts_n}} }, {q, {{dist_props_opts_q}} }]}]}, 75 | 76 | %% WEST Web Server. 77 | %% yaws: YAWS Web Server 78 | %% cowboy: Cowboy Web Server (Lightweight) 79 | %% default: 'cowboy' for single node, and 'yaws' for distributed WEST 80 | {web_server, {{web_server}} }, 81 | 82 | %% HTTP WebSocket Handshake Callback Spec 83 | %% 84 | %% http_ws_handshake_callback = {Module :: atom(), Function :: atom()} 85 | %% 86 | %% This callback is invoked when WS protocol handshake is initialized. 87 | %% In case of YAWS, the parameter 'Arg' is passed to the callback. 88 | %% In case of Cowboy, the parameter 'Req' is passed to the callback. 89 | %% The callback is invoked in this way: 90 | %% 91 | %% apply(Module, Function, [A]) -> Response. 92 | %% 93 | %% Where: 94 | %% Module = atom() - Module name 95 | %% Function = atom() - Function name 96 | %% A = #arg{} (YAWS) | #http_req{} (Cowboy) 97 | %% Response = ok | {ResponseCode, ReasonPhrase} | any() 98 | %% ResponseCode = integer() 99 | %% ReasonPhrase = binary() 100 | %% 101 | %% If the applied function returns 'ok' then the upgrade fromm HTTP 102 | %% to WebSocket continues, other cases upgrade is refused. 103 | {http_ws_handshake_callback, {{http_ws_handshake_callback}} } 104 | ] 105 | } 106 | ]. 107 | 108 | -------------------------------------------------------------------------------- /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+"$@"} -------------------------------------------------------------------------------- /rel/files/nodetool: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- 2 | %% ex: ft=erlang ts=4 sw=4 et 3 | %% ------------------------------------------------------------------- 4 | %% 5 | %% nodetool: Helper Script for interacting with live nodes 6 | %% 7 | %% ------------------------------------------------------------------- 8 | 9 | main(Args) -> 10 | ok = start_epmd(), 11 | %% Extract the args 12 | {RestArgs, TargetNode} = process_args(Args, [], undefined), 13 | 14 | %% See if the node is currently running -- if it's not, we'll bail 15 | case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of 16 | {true, pong} -> 17 | ok; 18 | {_, pang} -> 19 | io:format("Node ~p not responding to pings.\n", [TargetNode]), 20 | halt(1) 21 | end, 22 | 23 | case RestArgs of 24 | ["ping"] -> 25 | %% If we got this far, the node already responsed to a ping, so just dump 26 | %% a "pong" 27 | io:format("pong\n"); 28 | ["stop"] -> 29 | io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]); 30 | ["restart"] -> 31 | io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]); 32 | ["reboot"] -> 33 | io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]); 34 | ["rpc", Module, Function | RpcArgs] -> 35 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), 36 | [RpcArgs], 60000) of 37 | ok -> 38 | ok; 39 | {badrpc, Reason} -> 40 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 41 | halt(1); 42 | _ -> 43 | halt(1) 44 | end; 45 | ["rpcterms", Module, Function, ArgsAsString] -> 46 | case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), 47 | consult(ArgsAsString), 60000) of 48 | {badrpc, Reason} -> 49 | io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), 50 | halt(1); 51 | Other -> 52 | io:format("~p\n", [Other]) 53 | end; 54 | Other -> 55 | io:format("Other: ~p\n", [Other]), 56 | io:format("Usage: nodetool {ping|stop|restart|reboot}\n") 57 | end, 58 | net_kernel:stop(). 59 | 60 | process_args([], Acc, TargetNode) -> 61 | {lists:reverse(Acc), TargetNode}; 62 | process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) -> 63 | erlang:set_cookie(node(), list_to_atom(Cookie)), 64 | process_args(Rest, Acc, TargetNode); 65 | process_args(["-name", TargetName | Rest], Acc, _) -> 66 | ThisNode = append_node_suffix(TargetName, "_maint_"), 67 | {ok, _} = net_kernel:start([ThisNode, longnames]), 68 | process_args(Rest, Acc, nodename(TargetName)); 69 | process_args(["-sname", TargetName | Rest], Acc, _) -> 70 | ThisNode = append_node_suffix(TargetName, "_maint_"), 71 | {ok, _} = net_kernel:start([ThisNode, shortnames]), 72 | process_args(Rest, Acc, nodename(TargetName)); 73 | process_args([Arg | Rest], Acc, Opts) -> 74 | process_args(Rest, [Arg | Acc], Opts). 75 | 76 | 77 | start_epmd() -> 78 | [] = os:cmd(epmd_path() ++ " -daemon"), 79 | ok. 80 | 81 | epmd_path() -> 82 | ErtsBinDir = filename:dirname(escript:script_name()), 83 | Name = "epmd", 84 | case os:find_executable(Name, ErtsBinDir) of 85 | false -> 86 | case os:find_executable(Name) of 87 | false -> 88 | io:format("Could not find epmd.~n"), 89 | halt(1); 90 | GlobalEpmd -> 91 | GlobalEpmd 92 | end; 93 | Epmd -> 94 | Epmd 95 | end. 96 | 97 | 98 | nodename(Name) -> 99 | case string:tokens(Name, "@") of 100 | [_Node, _Host] -> 101 | list_to_atom(Name); 102 | [Node] -> 103 | [_, Host] = string:tokens(atom_to_list(node()), "@"), 104 | list_to_atom(lists:concat([Node, "@", Host])) 105 | end. 106 | 107 | append_node_suffix(Name, Suffix) -> 108 | case string:tokens(Name, "@") of 109 | [Node, Host] -> 110 | list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host])); 111 | [Node] -> 112 | list_to_atom(lists:concat([Node, Suffix, os:getpid()])) 113 | end. 114 | 115 | 116 | %% 117 | %% Given a string or binary, parse it into a list of terms, ala file:consult/0 118 | %% 119 | consult(Str) when is_list(Str) -> 120 | consult([], Str, []); 121 | consult(Bin) when is_binary(Bin)-> 122 | consult([], binary_to_list(Bin), []). 123 | 124 | consult(Cont, Str, Acc) -> 125 | case erl_scan:tokens(Cont, Str, 0) of 126 | {done, Result, Remaining} -> 127 | case Result of 128 | {ok, Tokens, _} -> 129 | {ok, Term} = erl_parse:parse_term(Tokens), 130 | consult([], Remaining, [Term | Acc]); 131 | {eof, _Other} -> 132 | lists:reverse(Acc); 133 | {error, Info, _} -> 134 | {error, Info} 135 | end; 136 | {more, Cont1} -> 137 | consult(Cont1, eof, Acc) 138 | end. 139 | -------------------------------------------------------------------------------- /rel/files/sys.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 | 12 | -------------------------------------------------------------------------------- /rel/files/vm.args: -------------------------------------------------------------------------------- 1 | ## Name of the node 2 | -name {{node}} 3 | 4 | ## Cookie for distributed erlang 5 | -setcookie west 6 | 7 | ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive 8 | ## (Disabled by default..use with caution!) 9 | ##-heart 10 | 11 | ## Enable kernel poll and a few async threads 12 | +K true 13 | +A 5 14 | 15 | ## Increase number of concurrent ports/sockets 16 | -env ERL_MAX_PORTS 4096 17 | 18 | ## Tweak GC to run more often 19 | -env ERL_FULLSWEEP_AFTER 10 20 | -------------------------------------------------------------------------------- /rel/files/west: -------------------------------------------------------------------------------- 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 | # Note the trailing slash on $PIPE_DIR/ 11 | PIPE_DIR=/tmp/$RUNNER_BASE_DIR/ 12 | RUNNER_USER= 13 | 14 | # Make sure this script is running as the appropriate user 15 | if [ ! -z "$RUNNER_USER" ] && [ `whoami` != "$RUNNER_USER" ]; then 16 | exec sudo -u $RUNNER_USER -i $0 $@ 17 | fi 18 | 19 | # Make sure CWD is set to runner base dir 20 | cd $RUNNER_BASE_DIR 21 | 22 | # Make sure log directory exists 23 | mkdir -p $RUNNER_LOG_DIR 24 | 25 | # Extract the target node name from node.args 26 | NAME_ARG=`grep -e '-[s]*name' $RUNNER_ETC_DIR/vm.args` 27 | if [ -z "$NAME_ARG" ]; then 28 | echo "vm.args needs to have either -name or -sname parameter." 29 | exit 1 30 | fi 31 | 32 | # Extract the target cookie 33 | COOKIE_ARG=`grep -e '-setcookie' $RUNNER_ETC_DIR/vm.args` 34 | if [ -z "$COOKIE_ARG" ]; then 35 | echo "vm.args needs to have a -setcookie parameter." 36 | exit 1 37 | fi 38 | 39 | # Identify the script name 40 | SCRIPT=`basename $0` 41 | 42 | # Parse out release and erts info 43 | START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` 44 | ERTS_VSN=${START_ERL% *} 45 | APP_VSN=${START_ERL#* } 46 | 47 | # Add ERTS bin dir to our path 48 | ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin 49 | 50 | # Setup command to control the node 51 | NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" 52 | 53 | # Check the first argument for instructions 54 | case "$1" in 55 | start) 56 | # Make sure there is not already a node running 57 | RES=`$NODETOOL ping` 58 | if [ "$RES" = "pong" ]; then 59 | echo "Node is already running!" 60 | exit 1 61 | fi 62 | HEART_COMMAND="$RUNNER_BASE_DIR/bin/$SCRIPT start" 63 | export HEART_COMMAND 64 | mkdir -p $PIPE_DIR 65 | shift # remove $1 66 | $ERTS_PATH/run_erl -daemon $PIPE_DIR $RUNNER_LOG_DIR "exec $RUNNER_BASE_DIR/bin/$SCRIPT console $@" 2>&1 67 | ;; 68 | 69 | stop) 70 | # Wait for the node to completely stop... 71 | case `uname -s` in 72 | Linux|Darwin|FreeBSD|DragonFly|NetBSD|OpenBSD) 73 | # PID COMMAND 74 | PID=`ps ax -o pid= -o command=|\ 75 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` 76 | ;; 77 | SunOS) 78 | # PID COMMAND 79 | PID=`ps -ef -o pid= -o args=|\ 80 | grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $1}'` 81 | ;; 82 | CYGWIN*) 83 | # UID PID PPID TTY STIME COMMAND 84 | PID=`ps -efW|grep "$RUNNER_BASE_DIR/.*/[b]eam"|awk '{print $2}'` 85 | ;; 86 | esac 87 | $NODETOOL stop 88 | while `kill -0 $PID 2>/dev/null`; 89 | do 90 | sleep 1 91 | done 92 | ;; 93 | 94 | restart) 95 | ## Restart the VM without exiting the process 96 | $NODETOOL restart 97 | ;; 98 | 99 | reboot) 100 | ## Restart the VM completely (uses heart to restart it) 101 | $NODETOOL reboot 102 | ;; 103 | 104 | ping) 105 | ## See if the VM is alive 106 | $NODETOOL ping 107 | ;; 108 | 109 | attach) 110 | # Make sure a node IS running 111 | RES=`$NODETOOL ping` 112 | if [ "$RES" != "pong" ]; then 113 | echo "Node is not running!" 114 | exit 1 115 | fi 116 | 117 | shift 118 | $ERTS_PATH/to_erl $PIPE_DIR 119 | ;; 120 | 121 | console|console_clean) 122 | # .boot file typically just $SCRIPT (ie, the app name) 123 | # however, for debugging, sometimes start_clean.boot is useful: 124 | case "$1" in 125 | console) BOOTFILE=$SCRIPT ;; 126 | console_clean) BOOTFILE=start_clean ;; 127 | esac 128 | # Setup beam-required vars 129 | ROOTDIR=$RUNNER_BASE_DIR 130 | BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin 131 | EMU=beam 132 | PROGNAME=`echo $0 | sed 's/.*\\///'` 133 | CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$BOOTFILE -embedded -config $RUNNER_ETC_DIR/app.config -args_file $RUNNER_ETC_DIR/vm.args -- ${1+"$@"}" 134 | export EMU 135 | export ROOTDIR 136 | export BINDIR 137 | export PROGNAME 138 | 139 | # Dump environment info for logging purposes 140 | echo "Exec: $CMD" 141 | echo "Root: $ROOTDIR" 142 | 143 | # Log the startup 144 | logger -t "$SCRIPT[$$]" "Starting up" 145 | 146 | # Start the VM 147 | exec $CMD 148 | ;; 149 | 150 | *) 151 | echo "Usage: $SCRIPT {start|stop|restart|reboot|ping|console|console_clean|attach}" 152 | exit 1 153 | ;; 154 | esac 155 | 156 | exit 0 157 | -------------------------------------------------------------------------------- /rel/files/west-admin: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RUNNER_SCRIPT_DIR=$(cd ${0%/*} && pwd) 4 | RUNNER_SCRIPT=${0##*/} 5 | 6 | RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*} 7 | RUNNER_ETC_DIR=$RUNNER_BASE_DIR/etc 8 | RUNNER_LOG_DIR=$RUNNER_BASE_DIR/log 9 | RUNNER_USER= 10 | 11 | # Make sure this script is running as the appropriate user 12 | if [ "$RUNNER_USER" -a "x$LOGNAME" != "x$RUNNER_USER" ]; then 13 | type -p sudo > /dev/null 2>&1 14 | if [ $? -ne 0 ]; then 15 | echo "sudo doesn't appear to be installed and your EUID isn't $RUNNER_USER" 1>&2 16 | exit 1 17 | fi 18 | echo "Attempting to restart script through sudo -u $RUNNER_USER" 19 | exec sudo -u $RUNNER_USER -i $RUNNER_SCRIPT_DIR/$RUNNER_SCRIPT $@ 20 | fi 21 | 22 | # Make sure CWD is set to runner base dir 23 | cd $RUNNER_BASE_DIR 24 | 25 | # Extract the target node name from node.args 26 | NAME_ARG=`grep -e '-[s]*name' $RUNNER_ETC_DIR/vm.args` 27 | if [ -z "$NAME_ARG" ]; then 28 | echo "vm.args needs to have either -name or -sname parameter." 29 | exit 1 30 | fi 31 | 32 | # Learn how to specify node name for connection from remote nodes 33 | echo "$NAME_ARG" | grep '^-sname' > /dev/null 2>&1 34 | if [ "X$?" = "X0" ]; then 35 | NAME_PARAM="-sname" 36 | NAME_HOST="" 37 | else 38 | NAME_PARAM="-name" 39 | echo "$NAME_ARG" | grep '@.*' > /dev/null 2>&1 40 | if [ "X$?" = "X0" ]; then 41 | NAME_HOST=`echo "${NAME_ARG}" | sed -e 's/.*(@.*)$//'` 42 | else 43 | NAME_HOST="" 44 | fi 45 | fi 46 | 47 | # Extract the target cookie 48 | COOKIE_ARG=`grep '-setcookie' $RUNNER_ETC_DIR/vm.args` 49 | if [ -z "$COOKIE_ARG" ]; then 50 | echo "vm.args needs to have a -setcookie parameter." 51 | exit 1 52 | fi 53 | 54 | # Identify the script name 55 | SCRIPT=`basename $0` 56 | 57 | # Parse out release and erts info 58 | START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` 59 | ERTS_VSN=${START_ERL% *} 60 | APP_VSN=${START_ERL#* } 61 | 62 | # Add ERTS bin dir to our path 63 | ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin 64 | 65 | # Setup command to control the node 66 | NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" 67 | 68 | # Check the first argument for instructions 69 | case "$1" in 70 | join) 71 | # Make sure the local node IS running 72 | RES=`$NODETOOL ping` 73 | if [ "$RES" != "pong" ]; then 74 | echo "Node is not running!" 75 | exit 1 76 | fi 77 | 78 | shift 79 | 80 | $NODETOOL rpc west_dist_console join $@ 81 | ;; 82 | 83 | leave) 84 | # Make sure the local node is running 85 | RES=`$NODETOOL ping` 86 | if [ "$RES" != "pong" ]; then 87 | echo "Node is not running!" 88 | exit 1 89 | fi 90 | 91 | shift 92 | $NODETOOL rpc west_dist_console leave $@ 93 | ;; 94 | 95 | remove) 96 | if [ $# -ne 2 ]; then 97 | echo "Usage: $SCRIPT remove " 98 | exit 1 99 | fi 100 | 101 | RES=`$NODETOOL ping` 102 | if [ "$RES" != "pong" ]; then 103 | echo "Node is not running!" 104 | exit 1 105 | fi 106 | 107 | shift 108 | $NODETOOL rpc west_dist_console remove $@ 109 | ;; 110 | 111 | ringready) 112 | # Make sure the local node IS running 113 | RES=`$NODETOOL ping` 114 | if [ "$RES" != "pong" ]; then 115 | echo "Node is not running!" 116 | exit 1 117 | fi 118 | shift 119 | 120 | $NODETOOL rpc west_dist_console ringready $@ 121 | ;; 122 | 123 | *) 124 | echo "Usage: $SCRIPT { join | leave | reip | ringready | remove }" 125 | exit 1 126 | ;; 127 | esac 128 | -------------------------------------------------------------------------------- /rel/reltool.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | %% ex: ft=erlang 3 | {sys, [ 4 | {lib_dirs, ["../deps"]}, 5 | {erts, [{mod_cond, derived}, {app_file, strip}]}, 6 | {app_file, strip}, 7 | {rel, "west", "0.2.1", 8 | [ 9 | kernel, 10 | stdlib, 11 | sasl, 12 | crypto, 13 | gproc, 14 | west 15 | ]}, 16 | {rel, "start_clean", "", 17 | [ 18 | kernel, 19 | stdlib 20 | ]}, 21 | {boot_rel, "west"}, 22 | {profile, embedded}, 23 | {incl_cond, derived}, 24 | {excl_archive_filters, [".*"]}, %% Do not archive built libs 25 | {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)", 26 | "^erts.*/(doc|info|include|lib|man|src)"]}, 27 | {excl_app_filters, ["\.gitignore"]}, 28 | {app, kernel, [{incl_cond, include}]}, 29 | {app, stdlib, [{incl_cond, include}]}, 30 | {app, sasl, [{incl_cond, include}]}, 31 | {app, crypto, [{incl_cond, include}]}, 32 | {app, gproc, [{incl_cond, include}]}, 33 | {app, west, [{mod_cond, app}, {incl_cond, include}, {lib_dir, ".."}]} 34 | ]}. 35 | 36 | {target_dir, "west"}. 37 | 38 | {overlay_vars, "vars.config"}. 39 | 40 | {overlay, [ 41 | {mkdir, "data/ring"}, 42 | {mkdir, "log/sasl"}, 43 | {copy, "files/erl", "{{erts_vsn}}/bin/erl"}, 44 | {copy, "files/nodetool", "{{erts_vsn}}/bin/nodetool"}, 45 | {copy, "../www", "."}, 46 | {template, "files/app.config", "etc/app.config"}, 47 | {template, "files/vm.args", "etc/vm.args"}, 48 | {template, "files/west", "bin/west"}, 49 | {template, "files/west-admin", "bin/west-admin"} 50 | ]}. 51 | -------------------------------------------------------------------------------- /rel/vars.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% etc/app.config 3 | %% 4 | 5 | %% Riak Core 6 | {ring_state_dir, "data/ring"}. 7 | {web_ip, "127.0.0.1"}. 8 | {web_port, "8888"}. 9 | {handoff_port, "8099"}. 10 | 11 | %% Common Server Properties 12 | {server_ebin_dir, "./lib/*/ebin"}. 13 | {server_ip, "{127,0,0,1}"}. 14 | {server_port, "8080"}. 15 | {server_docroot, "./www"}. 16 | {server_name, "west_server"}. 17 | 18 | %% Cowboy 19 | {cowboy_ws_resource, "/websocket"}. 20 | {cowboy_c_acceptors, "100"}. 21 | 22 | %% WEST 23 | {dist, "gproc"}. 24 | {dist_props_opts_n, "1"}. 25 | {dist_props_opts_q, "1"}. 26 | {http_ws_handshake_callback, "{none, none}"}. 27 | {web_server, "cowboy"}. 28 | 29 | %% 30 | %% etc/vm.args 31 | %% 32 | {node, "west@127.0.0.1"}. 33 | -------------------------------------------------------------------------------- /rel/vars/dev1.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% etc/app.config 3 | %% 4 | 5 | %% Riak Core 6 | {ring_state_dir, "data/ring"}. 7 | {web_ip, "127.0.0.1"}. 8 | {web_port, "8881"}. 9 | {handoff_port, "8101"}. 10 | 11 | %% Common Server Properties 12 | {server_ebin_dir, "./lib/*/ebin"}. 13 | {server_ip, "{127,0,0,1}"}. 14 | {server_port, "8081"}. 15 | {server_docroot, "./www"}. 16 | {server_name, "west_server"}. 17 | 18 | %% Cowboy 19 | {cowboy_ws_resource, "/websocket"}. 20 | {cowboy_c_acceptors, "100"}. 21 | 22 | %% WEST 23 | {dist, "west_dist"}. 24 | {dist_props_opts_n, "1"}. 25 | {dist_props_opts_q, "1"}. 26 | {http_ws_handshake_callback, "{none, none}"}. 27 | {web_server, "yaws"}. 28 | 29 | %% 30 | %% etc/vm.args 31 | %% 32 | {node, "west1@127.0.0.1"}. 33 | -------------------------------------------------------------------------------- /rel/vars/dev2.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% etc/app.config 3 | %% 4 | 5 | %% Riak Core 6 | {ring_state_dir, "data/ring"}. 7 | {web_ip, "127.0.0.1"}. 8 | {web_port, "8882"}. 9 | {handoff_port, "8102"}. 10 | 11 | %% Common Server Properties 12 | {server_ebin_dir, "./lib/*/ebin"}. 13 | {server_ip, "{127,0,0,1}"}. 14 | {server_port, "8082"}. 15 | {server_docroot, "./www"}. 16 | {server_name, "west_server"}. 17 | 18 | %% Cowboy 19 | {cowboy_ws_resource, "/websocket"}. 20 | {cowboy_c_acceptors, "100"}. 21 | 22 | %% WEST 23 | {dist, "west_dist"}. 24 | {dist_props_opts_n, "1"}. 25 | {dist_props_opts_q, "1"}. 26 | {http_ws_handshake_callback, "{none, none}"}. 27 | {web_server, "yaws"}. 28 | 29 | %% 30 | %% etc/vm.args 31 | %% 32 | {node, "west2@127.0.0.1"}. 33 | -------------------------------------------------------------------------------- /rel/vars/dev3.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% etc/app.config 3 | %% 4 | 5 | %% Riak Core 6 | {ring_state_dir, "data/ring"}. 7 | {web_ip, "127.0.0.1"}. 8 | {web_port, "8883"}. 9 | {handoff_port, "8103"}. 10 | 11 | %% Common Server Properties 12 | {server_ebin_dir, "./lib/*/ebin"}. 13 | {server_ip, "{127,0,0,1}"}. 14 | {server_port, "8083"}. 15 | {server_docroot, "./www"}. 16 | {server_name, "west_server"}. 17 | 18 | %% Cowboy 19 | {cowboy_ws_resource, "/websocket"}. 20 | {cowboy_c_acceptors, "100"}. 21 | 22 | %% WEST 23 | {dist, "west_dist"}. 24 | {dist_props_opts_n, "1"}. 25 | {dist_props_opts_q, "1"}. 26 | {http_ws_handshake_callback, "{none, none}"}. 27 | {web_server, "yaws"}. 28 | 29 | %% 30 | %% etc/vm.args 31 | %% 32 | {node, "west3@127.0.0.1"}. 33 | -------------------------------------------------------------------------------- /src/endpoints/cowboy/west_cowboy_ws_json_handler.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc JSON Wire Protocol. 25 | %%% @see Cowboy Sources 26 | %%% @end 27 | %%% Created : 23. Jul 2014 2:15 PM 28 | %%%------------------------------------------------------------------- 29 | -module(west_cowboy_ws_json_handler). 30 | 31 | -behaviour(cowboy_websocket_handler). 32 | 33 | %% Export for websocket callbacks 34 | -export([init/3, 35 | websocket_init/3, 36 | websocket_handle/3, 37 | websocket_info/3, 38 | websocket_terminate/3]). 39 | 40 | %% Callback 41 | -export([ev_callback/2]). 42 | 43 | -include("west.hrl"). 44 | -include("../../west_protocol.hrl"). 45 | 46 | -record(state, {server = ?WEST{}, nb_texts = 0, nb_bins = 0}). 47 | 48 | %%%=================================================================== 49 | %%% WS callback 50 | %%%=================================================================== 51 | 52 | init({_, http}, Req, _Opts) -> 53 | case application:get_env(west, http_ws_handshake_callback) of 54 | {ok, {Mod, Fun}} when Mod =/= none, Fun =/= none -> 55 | ?LOG_INFO("apply(~p, ~p)~n", [Mod, Fun]), 56 | case apply(Mod, Fun, [Req]) of 57 | ok -> 58 | {upgrade, protocol, cowboy_websocket}; 59 | {Rc, Rp} when is_integer(Rc), is_binary(Rp) -> 60 | {ok, Req2} = cowboy_req:reply(Rc, [], Rp, Req), 61 | {ok, Req2, #state{}}; 62 | _ -> 63 | {ok, Req3} = cowboy_req:reply(401, [], <<>>, Req), 64 | {ok, Req3, #state{}} 65 | end; 66 | _ -> 67 | {upgrade, protocol, cowboy_websocket} 68 | end. 69 | 70 | websocket_init(_TransportName, Req, _Opts) -> 71 | ?LOG_INFO("Initalize ~p: ~p~n", [self(), Req]), 72 | Dist = application:get_env(west, dist, gproc), 73 | Scope = ?GPROC_SCOPE(Dist), 74 | DistProps = application:get_env(west, dist_props, [{opts, [{n, 1}, {q, 1}]}]), 75 | case cowboy_req:binding(key, Req) of 76 | {Key, _} -> 77 | Name = west_util:build_name([Key, self(), west_util:get_timestamp_ms()]), 78 | register(Name, self()), 79 | CbSpec = {?MODULE, ev_callback, [{Name, node()}, undefined]}, 80 | {ok, Req, #state{server = ?WEST{name = Name, 81 | key = Key, 82 | dist = Dist, 83 | dist_props = DistProps, 84 | scope = Scope, 85 | cb = CbSpec, 86 | encoding = json}}}; 87 | _ -> 88 | {shutdown, Req} 89 | end. 90 | 91 | websocket_handle({text, <<"bye">>}, Req, #state{nb_texts = N, nb_bins = M} = S) -> 92 | ?LOG_INFO("bye - Msg processed: ~p text, ~p binary~n", [N, M]), 93 | {shutdown, Req, S}; 94 | websocket_handle({text, Msg}, 95 | Req, 96 | #state{nb_texts = N, server = ?WEST{key = K}} = S) -> 97 | ?LOG_INFO("Received text msg (N=~p): ~p bytes~n", [N, byte_size(Msg)]), 98 | case west_msg:dec_msg(Msg, json) of 99 | {error, Reason} -> 100 | {reply, {text, Reason}, S#state{nb_texts = N + 1}}; 101 | ParsedMsg -> 102 | ?LOG_INFO( 103 | "[~p] ~p ~p~n", 104 | [K, ParsedMsg#msg_t.event, ParsedMsg#msg_t.channel]), 105 | Cmd = binary_to_atom(ParsedMsg#msg_t.event, utf8), 106 | case west_protocol_handler:handle_event(Cmd, ParsedMsg, S#state.server) of 107 | {ok, Response} -> 108 | {reply, {text, Response}, Req, S#state{nb_texts = N + 1}}; 109 | {error, Err0} -> 110 | {reply, {text, Err0}, Req, S#state{nb_texts = N + 1}}; 111 | _ -> 112 | ?MSG{id = Id, channel = Ch} = ParsedMsg, 113 | Err1 = ?RES_ACTION_NOT_ALLOWED(Id, Ch, json), 114 | {reply, {text, Err1}, Req, S#state{nb_texts = N + 1}} 115 | end 116 | end; 117 | websocket_handle({binary, Msg}, Req, #state{nb_bins = M} = S) -> 118 | ?LOG_INFO("Received binary msg (M=~p): ~p bytes~n", [M, byte_size(Msg)]), 119 | {reply, {binary, <<"bad_encoding">>}, Req, S#state{nb_bins = M + 1}}; 120 | websocket_handle(_Data, Req, S) -> 121 | {ok, Req, S}. 122 | 123 | websocket_info({event, Msg}, Req, S) -> 124 | {reply, {text, Msg}, Req, S}; 125 | websocket_info(_Info, Req, S) -> 126 | {ok, Req, S}. 127 | 128 | websocket_terminate(Reason, _Req, S) -> 129 | ?LOG_INFO("terminate ~p: ~p (state:~p)~n", [self(), Reason, S]), 130 | ok. 131 | 132 | %%%=================================================================== 133 | %%% Callback 134 | %%%=================================================================== 135 | 136 | %% @private 137 | %% @doc Event callback. This function is executed when message arrives. 138 | ev_callback({ETag, Event, Msg}, [WSRef, Id]) -> 139 | Reply = ?RES_CH_NEW_MSG(Id, ETag, Event, Msg, json), 140 | WSRef ! {event, Reply}. 141 | -------------------------------------------------------------------------------- /src/endpoints/cowboy/west_cowboy_ws_pb_handler.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Protocol Buffers Handler. 25 | %%% @see 26 | %%% Cowboy Sources 27 | %%% 28 | %%% Google Protocol Buffers 29 | %%% @end 30 | %%% Created : 22. Jul 2014 10:00 AM 31 | %%%------------------------------------------------------------------- 32 | -module(west_cowboy_ws_pb_handler). 33 | 34 | -behaviour(cowboy_websocket_handler). 35 | 36 | %% Export for websocket callbacks 37 | -export([init/3, 38 | websocket_init/3, 39 | websocket_handle/3, 40 | websocket_info/3, 41 | websocket_terminate/3]). 42 | 43 | %% Callback 44 | -export([ev_callback/2]). 45 | 46 | -include("west.hrl"). 47 | -include("../../west_protocol.hrl"). 48 | 49 | -record(state, {server = ?WEST{}, nb_texts = 0, nb_bins = 0}). 50 | 51 | %%%=================================================================== 52 | %%% WS callback 53 | %%%=================================================================== 54 | 55 | init({_, http}, Req, _Opts) -> 56 | case application:get_env(west, http_ws_handshake_callback) of 57 | {ok, {Mod, Fun}} when Mod =/= none, Fun =/= none -> 58 | ?LOG_INFO("apply(~p, ~p)~n", [Mod, Fun]), 59 | case apply(Mod, Fun, [Req]) of 60 | ok -> 61 | {upgrade, protocol, cowboy_websocket}; 62 | {Rc, Rp} when is_integer(Rc), is_binary(Rp) -> 63 | {ok, Req2} = cowboy_req:reply(Rc, [], Rp, Req), 64 | {ok, Req2, #state{}}; 65 | _ -> 66 | {ok, Req3} = cowboy_req:reply(401, [], <<>>, Req), 67 | {ok, Req3, #state{}} 68 | end; 69 | _ -> 70 | {upgrade, protocol, cowboy_websocket} 71 | end. 72 | 73 | websocket_init(_TransportName, Req, _Opts) -> 74 | ?LOG_INFO("Initalize ~p: ~p~n", [self(), Req]), 75 | Dist = application:get_env(west, dist, gproc), 76 | Scope = ?GPROC_SCOPE(Dist), 77 | DistProps = application:get_env(west, dist_props, [{opts, [{n, 1}, {q, 1}]}]), 78 | case cowboy_req:binding(key, Req) of 79 | {Key, _} -> 80 | Name = west_util:build_name([Key, self(), west_util:get_timestamp_ms()]), 81 | register(Name, self()), 82 | CbSpec = {?MODULE, ev_callback, [{Name, node()}, undefined]}, 83 | {ok, Req, #state{server = ?WEST{name = Name, 84 | key = Key, 85 | dist = Dist, 86 | dist_props = DistProps, 87 | scope = Scope, 88 | cb = CbSpec, 89 | encoding = pb}}}; 90 | _ -> 91 | {shutdown, Req} 92 | end. 93 | 94 | websocket_handle({binary, Msg}, 95 | Req, 96 | #state{nb_bins = M, server = ?WEST{key = K}} = S) -> 97 | ?LOG_INFO("Received binary msg (M=~p): ~p bytes~n", [M, byte_size(Msg)]), 98 | try 99 | DecMsg = message_pb:decode_message(Msg), 100 | Cmd = west_util:to_atom(DecMsg#msg_t.event), 101 | ?LOG_INFO( 102 | "[~p] ~p ~p~n", [K, DecMsg#msg_t.event, DecMsg#msg_t.channel]), 103 | case west_protocol_handler:handle_event(Cmd, DecMsg, S#state.server) of 104 | {ok, Res} -> 105 | BinRes = iolist_to_binary(message_pb:encode_message(Res)), 106 | {reply, {binary, BinRes}, Req, S#state{nb_bins = M + 1}}; 107 | {error, Err} -> 108 | BinErr = iolist_to_binary(message_pb:encode_message(Err)), 109 | {reply, {binary, BinErr}, Req, S#state{nb_bins = M + 1}}; 110 | _ -> 111 | Err1 = ?RES_ACTION_NOT_ALLOWED(DecMsg#msg_t.id, 112 | DecMsg#msg_t.channel, 113 | pb), 114 | BinErr1 = iolist_to_binary(message_pb:encode_message(Err1)), 115 | {reply, {binary, BinErr1}, Req, S#state{nb_bins = M + 1}} 116 | end 117 | catch 118 | _:_ -> 119 | Err2 = ?RES_BAD_REQUEST(pb), 120 | BinErr2 = iolist_to_binary(message_pb:encode_message(Err2)), 121 | {reply, {binary, BinErr2}, Req, S#state{nb_bins = M + 1}} 122 | end; 123 | websocket_handle({text, Msg}, Req, #state{nb_texts = N} = S) -> 124 | ?LOG_INFO("Received text msg (N=~p): ~p bytes~n", [N, byte_size(Msg)]), 125 | {reply, {text, <<"bad_encoding">>}, Req, S}; 126 | websocket_handle(_Data, Req, S) -> 127 | {ok, Req, S}. 128 | 129 | websocket_info({event, Msg}, Req, S) -> 130 | {reply, {binary, Msg}, Req, S}; 131 | websocket_info(_Info, Req, S) -> 132 | {ok, Req, S}. 133 | 134 | websocket_terminate(Reason, _Req, S) -> 135 | ?LOG_INFO("terminate ~p: ~p (state:~p)~n", [self(), Reason, S]), 136 | ok. 137 | 138 | %%%=================================================================== 139 | %%% Callback 140 | %%%=================================================================== 141 | 142 | %% @private 143 | %% @doc Event callback. This function is executed when message arrives. 144 | ev_callback({ETag, Event, Msg}, [WSRef, Id]) -> 145 | Reply = ?RES_CH_NEW_MSG(Id, ETag, atom_to_binary(Event, utf8), Msg, pb), 146 | BinReply = iolist_to_binary(message_pb:encode_message(Reply)), 147 | WSRef ! {event, BinReply}. 148 | -------------------------------------------------------------------------------- /src/endpoints/cowboy/west_cowboy_ws_text_handler.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Text Wire Protocol. 25 | %%% @see Cowboy Sources 26 | %%% @end 27 | %%% Created : 17. Jul 2014 10:14 AM 28 | %%%------------------------------------------------------------------- 29 | -module(west_cowboy_ws_text_handler). 30 | 31 | -behaviour(cowboy_websocket_handler). 32 | 33 | %% Export for websocket callbacks 34 | -export([init/3, 35 | websocket_init/3, 36 | websocket_handle/3, 37 | websocket_info/3, 38 | websocket_terminate/3]). 39 | 40 | %% Callback 41 | -export([ev_callback/2]). 42 | 43 | -include("west.hrl"). 44 | 45 | -record(state, {server = ?WEST{}, nb_texts = 0, nb_bins = 0}). 46 | 47 | %%%=================================================================== 48 | %%% WS callback 49 | %%%=================================================================== 50 | 51 | init({_, http}, Req, _Opts) -> 52 | case application:get_env(west, http_ws_handshake_callback) of 53 | {ok, {Mod, Fun}} when Mod =/= none, Fun =/= none -> 54 | ?LOG_INFO("apply(~p, ~p)~n", [Mod, Fun]), 55 | case apply(Mod, Fun, [Req]) of 56 | ok -> 57 | {upgrade, protocol, cowboy_websocket}; 58 | {Rc, Rp} when is_integer(Rc), is_binary(Rp) -> 59 | {ok, Req2} = cowboy_req:reply(Rc, [], Rp, Req), 60 | {ok, Req2, #state{}}; 61 | _ -> 62 | {ok, Req3} = cowboy_req:reply(401, [], <<>>, Req), 63 | {ok, Req3, #state{}} 64 | end; 65 | _ -> 66 | {upgrade, protocol, cowboy_websocket} 67 | end. 68 | 69 | websocket_init(_TransportName, Req, _Opts) -> 70 | ?LOG_INFO("Initalize ~p: ~p~n", [self(), Req]), 71 | Dist = application:get_env(west, dist, gproc), 72 | Scope = ?GPROC_SCOPE(Dist), 73 | DistProps = application:get_env(west, dist_props, [{opts, [{n, 1}, {q, 1}]}]), 74 | case cowboy_req:binding(key, Req) of 75 | {Key, _} -> 76 | Name = west_util:build_name([Key, self(), west_util:get_timestamp_ms()]), 77 | register(Name, self()), 78 | CbSpec = {?MODULE, ev_callback, [{Name, node()}, undefined]}, 79 | {ok, Req, #state{server = ?WEST{name = Name, 80 | key = Key, 81 | dist = Dist, 82 | dist_props = DistProps, 83 | scope = Scope, 84 | cb = CbSpec, 85 | encoding = text}}}; 86 | _ -> 87 | {shutdown, Req} 88 | end. 89 | 90 | websocket_handle({text, <<"bye">>}, Req, #state{nb_texts = N, nb_bins = M} = State) -> 91 | ?LOG_INFO("bye - Msg processed: ~p text, ~p binary~n", [N, M]), 92 | {shutdown, Req, State}; 93 | websocket_handle({text, <<"ping">>}, Req, State) -> 94 | {reply, {text, <<"pong">>}, Req, State}; 95 | websocket_handle({text, Msg}, Req, #state{nb_texts = N} = State) -> 96 | ?LOG_INFO("Received text msg (N=~p): ~p bytes~n", [N, byte_size(Msg)]), 97 | case dec_msg(Msg) of 98 | none -> 99 | {reply, {text, Msg}, State#state{nb_texts = N + 1}}; 100 | Cmd -> 101 | case handle_event(string:to_lower(Cmd), State#state.server) of 102 | {ok, Reason} -> 103 | {reply, {text, Reason}, Req, State#state{nb_texts = N + 1}}; 104 | {error, Err0} -> 105 | {reply, {text, Err0}, Req, State#state{nb_texts = N + 1}}; 106 | _ -> 107 | ErrMsg = <<"west:action_not_allowed">>, 108 | {reply, {text, ErrMsg}, Req, State#state{nb_texts = N + 1}} 109 | end 110 | end; 111 | websocket_handle(_Data, Req, State) -> 112 | {ok, Req, State}. 113 | 114 | websocket_info({event, Msg}, Req, State) -> 115 | {reply, {text, Msg}, Req, State}; 116 | websocket_info(_Info, Req, State) -> 117 | {ok, Req, State}. 118 | 119 | websocket_terminate(Reason, _Req, State) -> 120 | ?LOG_INFO("terminate ~p: ~p (state:~p)~n", [self(), Reason, State]), 121 | ok. 122 | 123 | %%%=================================================================== 124 | %%% Event handlers 125 | %%%=================================================================== 126 | 127 | %% @private 128 | %% @doc Handle the register event. 129 | handle_event(["reg", Ch], WS) -> 130 | MsgSpec = ?MSG{id = undefined, channel = Ch}, 131 | Res = west_protocol_handler:handle_event(register, MsgSpec, WS), 132 | {_, ?MSG{event = Event}} = Res, 133 | BinRes = <<(<<"west ">>)/binary, 134 | (iolist_to_binary(Ch ++ ":"))/binary, 135 | (iolist_to_binary(Event))/binary>>, 136 | {ok, BinRes}; 137 | 138 | %% @private 139 | %% @doc Handle the unregister event. 140 | handle_event(["unreg", Ch], WS) -> 141 | MsgSpec = ?MSG{id = undefined, channel = Ch}, 142 | Res = west_protocol_handler:handle_event(unregister, MsgSpec, WS), 143 | {_, ?MSG{event = Event}} = Res, 144 | BinRes = <<(<<"west ">>)/binary, 145 | (iolist_to_binary(Ch ++ ":"))/binary, 146 | (iolist_to_binary(Event))/binary>>, 147 | {ok, BinRes}; 148 | 149 | %% @private 150 | %% @doc Handle the send event. 151 | handle_event(["send", Ch, Msg], WS) -> 152 | MsgSpec = ?MSG{id = undefined, channel = Ch, data = Msg}, 153 | Res = west_protocol_handler:handle_event(send, MsgSpec, WS), 154 | {_, ?MSG{event = Event}} = Res, 155 | BinRes = <<(<<"west ">>)/binary, 156 | (iolist_to_binary(Ch ++ ":"))/binary, 157 | (iolist_to_binary(Event))/binary>>, 158 | {ok, BinRes}; 159 | 160 | %% @private 161 | %% @doc Handle the publish event. 162 | handle_event(["pub", Ch, Msg], WS) -> 163 | MsgSpec = ?MSG{id = undefined, channel = Ch, data = Msg}, 164 | Res = west_protocol_handler:handle_event(publish, MsgSpec, WS), 165 | {_, ?MSG{event = Event}} = Res, 166 | BinRes = <<(<<"west ">>)/binary, 167 | (iolist_to_binary(Ch ++ ":"))/binary, 168 | (iolist_to_binary(Event))/binary>>, 169 | {ok, BinRes}; 170 | 171 | %% @private 172 | %% @doc Handle the subscribe event. 173 | handle_event(["sub", Ch], WS) -> 174 | MsgSpec = ?MSG{id = undefined, channel = Ch}, 175 | Res = west_protocol_handler:handle_event(subscribe, MsgSpec, WS), 176 | {_, ?MSG{event = Event}} = Res, 177 | BinRes = <<(<<"west ">>)/binary, 178 | (iolist_to_binary(Ch ++ ":"))/binary, 179 | (iolist_to_binary(Event))/binary>>, 180 | {ok, BinRes}; 181 | 182 | %% @private 183 | %% @doc Handle the unsubscribe event. 184 | handle_event(["unsub", Ch], WS) -> 185 | MsgSpec = ?MSG{id = undefined, channel = Ch}, 186 | Res = west_protocol_handler:handle_event(unsubscribe, MsgSpec, WS), 187 | {_, ?MSG{event = Event}} = Res, 188 | BinRes = <<(<<"west ">>)/binary, 189 | (iolist_to_binary(Ch ++ ":"))/binary, 190 | (iolist_to_binary(Event))/binary>>, 191 | {ok, BinRes}; 192 | 193 | handle_event(Any, _State) -> 194 | {none, Any}. 195 | 196 | %%%=================================================================== 197 | %%% Internal functions 198 | %%%=================================================================== 199 | 200 | %% @private 201 | %% @doc Parse the text-based event. 202 | dec_msg(Msg) -> 203 | L = [string:strip(X, both, $ ) || X <- string:tokens(binary_to_list(Msg), "\"")], 204 | case L of 205 | [C, M] -> string:tokens(C, " ") ++ [M]; 206 | [C] -> string:tokens(C, " "); 207 | _ -> none 208 | end. 209 | 210 | %%%=================================================================== 211 | %%% Callback 212 | %%%=================================================================== 213 | 214 | %% @private 215 | %% @doc Event callback. This function is executed when message arrives. 216 | ev_callback({ETag, Event, Msg}, [WSRef, _Id]) -> 217 | Body = case Msg of 218 | Msg when is_binary(Msg) -> 219 | binary_to_list(Msg); 220 | _ -> 221 | Msg 222 | end, 223 | Reply = <<(iolist_to_binary(ETag))/binary, 224 | (<<" ">>)/binary, 225 | (atom_to_binary(Event, utf8))/binary, 226 | (iolist_to_binary(":new_message "))/binary, 227 | (iolist_to_binary(Body))/binary>>, 228 | WSRef ! {event, Reply}. 229 | -------------------------------------------------------------------------------- /src/endpoints/yaws/west_yaws_ws_endpoint.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc This module handle the WS initial handshaking. This module 25 | %%% was originally taken from `yaws' WS examples. 26 | %%% @end 27 | %%% Created : 03. Oct 2013 9:57 AM 28 | %%%------------------------------------------------------------------- 29 | -module(west_yaws_ws_endpoint). 30 | 31 | %% API 32 | -export([out/1]). 33 | 34 | -include("west.hrl"). 35 | 36 | out(A) -> 37 | %% HTTP WebSocket handshake callback 38 | Path = string:tokens(yaws_api:arg_pathinfo(A), "/"), 39 | case application:get_env(west, http_ws_handshake_callback) of 40 | {ok, {Mod, Fun}} when Mod =/= none, Fun =/= none -> 41 | ?LOG_INFO("apply(~p, ~p)~n", [Mod, Fun]), 42 | case apply(Mod, Fun, [A]) of 43 | ok -> 44 | handle(Path, A); 45 | {Rc, Rp} when is_integer(Rc), is_binary(Rp) -> 46 | [{status, Rc}, {html, Rp}]; 47 | _ -> 48 | [{status, 401}, {html, <<>>}] 49 | end; 50 | _ -> 51 | handle(Path, A) 52 | end. 53 | 54 | handle(["text", _Key], A) -> 55 | upgrade_http_to_websocket(west_yaws_ws_text_handler, A); 56 | handle(["json", _Key], A) -> 57 | upgrade_http_to_websocket(west_yaws_ws_json_handler, A); 58 | handle(["pb", _Key], A) -> 59 | upgrade_http_to_websocket(west_yaws_ws_pb_handler, A); 60 | handle(_, _) -> 61 | [{status, 404}, {html, <<>>}]. 62 | 63 | upgrade_http_to_websocket(CallbackMod, A) -> 64 | %% To enable keepalive timer add 'keepalive=true' in the query string. 65 | KeepAlive = case yaws_api:queryvar(A, "keepalive") of 66 | {ok, "true"} -> true; 67 | _ -> false 68 | end, 69 | 70 | %% To define a keepalive timeout value, add 'timeout=Int' in the query 71 | %% string. 72 | Tout = case yaws_api:queryvar(A, "timeout") of 73 | {ok, Val} -> 74 | try 75 | list_to_integer(Val) 76 | catch 77 | _:_ -> infinity 78 | end; 79 | _ -> 80 | infinity 81 | end, 82 | 83 | %% To drop connection when a timeout occured, add 'drop=true' in the query 84 | %% string. 85 | Drop = case yaws_api:queryvar(A, "drop") of 86 | {ok, "true"} -> true; 87 | _ -> false 88 | end, 89 | 90 | %% To reject unmasked frames , add 'close_unmasked=true' in the query 91 | %% string. 92 | CloseUnmasked = case yaws_api:queryvar(A, "close_unmasked") of 93 | {ok, "true"} -> true; 94 | _ -> false 95 | end, 96 | 97 | %% NOTE: change the line below to 98 | %% Opts = [{origin, any}], 99 | %% if you want to accept calls from any origin. 100 | Opts = [ 101 | {origin, any}, 102 | {keepalive, KeepAlive}, 103 | {keepalive_timeout, Tout}, 104 | {drop_on_timeout, Drop}, 105 | {close_if_unmasked, CloseUnmasked} 106 | ], 107 | {websocket, CallbackMod, Opts}. 108 | -------------------------------------------------------------------------------- /src/endpoints/yaws/west_yaws_ws_json_handler.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc JSON Wire Protocol. This module is the `yaws' extended 25 | %%% callback module. Here the WS messages are received and 26 | %%% handled. 27 | %%% @see Yaws Sources 28 | %%% @end 29 | %%% Created : 08. Nov 2013 3:05 AM 30 | %%%------------------------------------------------------------------- 31 | -module(west_yaws_ws_json_handler). 32 | 33 | %% Export for websocket callbacks 34 | -export([init/1, 35 | terminate/2, 36 | handle_open/2, 37 | handle_message/2, 38 | handle_info/2]). 39 | 40 | %% Callback 41 | -export([ev_callback/2]). 42 | 43 | -include("west.hrl"). 44 | -include("../../west_protocol.hrl"). 45 | 46 | -record(state, {server = ?WEST{}, nb_texts = 0, nb_bins = 0}). 47 | 48 | %%%=================================================================== 49 | %%% WS callback 50 | %%%=================================================================== 51 | 52 | %% @doc Initialize the internal state of the callback module. 53 | %% @see Yaws 54 | init([Arg, InitialState]) -> 55 | ?LOG_INFO("Initalize ~p: ~p~n", [self(), InitialState]), 56 | Dist = application:get_env(west, dist, gproc), 57 | Scope = ?GPROC_SCOPE(Dist), 58 | DistProps = application:get_env(west, dist_props, [{opts, [{n, 1}, {q, 1}]}]), 59 | case string:tokens(yaws_api:arg_pathinfo(Arg), "/") of 60 | [_, Key] -> 61 | Name = west_util:build_name([Key, self(), west_util:get_timestamp_ms()]), 62 | register(Name, self()), 63 | CbSpec = {?MODULE, ev_callback, [{Name, node()}, undefined]}, 64 | {ok, #state{server = ?WEST{name = Name, 65 | key = Key, 66 | dist = Dist, 67 | dist_props = DistProps, 68 | scope = Scope, 69 | cb = CbSpec, 70 | encoding = json}}}; 71 | _ -> 72 | Err = "{\"event\":\"bad_request\", " 73 | "\"data\":\"Error, missing key in path.\"}", 74 | {error, iolist_to_binary(Err)} 75 | end. 76 | 77 | %% @doc This function is called when the connection is upgraded from 78 | %% HTTP to WebSocket. 79 | %% @see Yaws 80 | handle_open(WSState, S) -> 81 | Response = ?RES_CONN_ESTABLISHED(json), 82 | yaws_websockets:send(WSState, {text, Response}), 83 | {ok, S}. 84 | 85 | %% @doc This function is called when a message <<"bye">> is received. 86 | %% @see Yaws 87 | handle_message({text, <<"bye">>}, #state{nb_texts = N, nb_bins = M} = S) -> 88 | ?LOG_INFO("bye - Msg processed: ~p text, ~p binary~n", [N, M]), 89 | NbTexts = list_to_binary(integer_to_list(N)), 90 | NbBins = list_to_binary(integer_to_list(M)), 91 | Messages = [{text, <<"Goodbye !">>}, 92 | {text, <>}, 93 | {text, <>}], 94 | {close, {1000, <<"bye">>}, Messages, S}; 95 | 96 | %% @doc This function is called when a TEXT message is received. 97 | %% @see Yaws 98 | handle_message({text, Msg}, 99 | #state{nb_texts = N, server = ?WEST{key = K}} = S) -> 100 | ?LOG_INFO("Received text msg (N=~p): ~p bytes~n", [N, byte_size(Msg)]), 101 | case west_msg:dec_msg(Msg, json) of 102 | {error, Reason} -> 103 | {reply, {text, Reason}, S#state{nb_texts = N + 1}}; 104 | ParsedMsg -> 105 | ?LOG_INFO( 106 | "[~p] ~p ~p~n", 107 | [K, ParsedMsg#msg_t.event, ParsedMsg#msg_t.channel]), 108 | Cmd = binary_to_atom(ParsedMsg#msg_t.event, utf8), 109 | case west_protocol_handler:handle_event(Cmd, ParsedMsg, S#state.server) of 110 | {ok, Response} -> 111 | {reply, {text, Response}, S#state{nb_texts = N + 1}}; 112 | {error, Err0} -> 113 | {reply, {text, Err0}, S#state{nb_texts = N + 1}}; 114 | _ -> 115 | ?MSG{id = Id, channel = Ch} = ParsedMsg, 116 | Err1 = ?RES_ACTION_NOT_ALLOWED(Id, Ch, json), 117 | {reply, {text, Err1}, S#state{nb_texts = N + 1}} 118 | end 119 | end; 120 | 121 | %% @doc This function is called when a binary message is received. 122 | %% NOT HANDLED by this handler. 123 | %% @see Yaws 124 | handle_message({binary, Msg}, #state{nb_bins = M} = S) -> 125 | ?LOG_INFO("Received binary msg (M=~p): ~p bytes~n", [M, byte_size(Msg)]), 126 | {reply, {binary, <<"bad_encoding">>}, S#state{nb_bins = M + 1}}; 127 | 128 | %% @doc When the client closes the connection, the callback module is 129 | %% notified with the message {close, Status, Reason} 130 | %% @see Yaws 131 | handle_message({close, Status, Reason}, _) -> 132 | ?LOG_INFO("Close connection: ~p - ~p~n", [Status, Reason]), 133 | {close, Status}. 134 | 135 | %% @doc 136 | %% If defined, this function is called when a timeout occurs or when 137 | %% the handling process receives any unknown message. 138 | %%
139 | %% Info is either the atom timeout, if a timeout has occurred, or 140 | %% the received message. 141 | %% @see Yaws 142 | handle_info(timeout, S) -> 143 | ?LOG_INFO("process timed out~n", []), 144 | {reply, {text, <<"{\"event\":\"timeout\"}">>}, S}; 145 | handle_info(_Info, S) -> 146 | {noreply, S}. 147 | 148 | %% @doc This function is called when the handling process is about to 149 | %% terminate. it should be the opposite of Module:init/1 and do 150 | %% any necessary cleaning up. 151 | %% @see Yaws 152 | terminate(Reason, S) -> 153 | ?LOG_INFO("terminate ~p: ~p (state:~p)~n", [self(), Reason, S]), 154 | ok. 155 | 156 | %%%=================================================================== 157 | %%% Callback 158 | %%%=================================================================== 159 | 160 | %% @private 161 | %% @doc Event callback. This function is executed when message arrives. 162 | ev_callback({ETag, Event, Msg}, [WSRef, Id]) -> 163 | Reply = ?RES_CH_NEW_MSG(Id, ETag, Event, Msg, json), 164 | yaws_api:websocket_send(WSRef, {text, Reply}). 165 | -------------------------------------------------------------------------------- /src/endpoints/yaws/west_yaws_ws_pb_handler.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Protocol Buffers Handler 25 | %%% @see 26 | %%% 27 | %%% Google Protocol Buffers 28 | %%% 29 | %%% @end 30 | %%% Created : 13. Apr 2014 12:26 PM 31 | %%%------------------------------------------------------------------- 32 | -module(west_yaws_ws_pb_handler). 33 | 34 | %% Export for websocket callbacks 35 | -export([init/1, 36 | terminate/2, 37 | handle_open/2, 38 | handle_message/2, 39 | handle_info/2]). 40 | 41 | %% Callback 42 | -export([ev_callback/2]). 43 | 44 | -include("west.hrl"). 45 | -include("../../west_protocol.hrl"). 46 | 47 | -record(state, {server = ?WEST{}, nb_texts = 0, nb_bins = 0}). 48 | 49 | %%%=================================================================== 50 | %%% WS callback 51 | %%%=================================================================== 52 | 53 | %% @doc Initialize the internal state of the callback module. 54 | %% @see Yaws 55 | init([Arg, InitialState]) -> 56 | ?LOG_INFO("Initalize ~p: ~p~n", [self(), InitialState]), 57 | Dist = application:get_env(west, dist, gproc), 58 | Scope = ?GPROC_SCOPE(Dist), 59 | DistProps = application:get_env(west, dist_props, [{opts, [{n, 1}, {q, 1}]}]), 60 | case string:tokens(yaws_api:arg_pathinfo(Arg), "/") of 61 | [_, Key] -> 62 | Name = west_util:build_name([Key, self(), west_util:get_timestamp_ms()]), 63 | register(Name, self()), 64 | CbSpec = {?MODULE, ev_callback, [{Name, node()}, undefined]}, 65 | {ok, #state{server = ?WEST{name = Name, 66 | key = Key, 67 | dist = Dist, 68 | dist_props = DistProps, 69 | scope = Scope, 70 | cb = CbSpec, 71 | encoding = pb}}}; 72 | _ -> 73 | Err = ?MSG{event = "bad_request", 74 | data = <<"Error, missing key in path.">>}, 75 | {error, iolist_to_binary(message_pb:encode_message(Err))} 76 | end. 77 | 78 | %% @doc This function is called when the connection is upgraded from 79 | %% HTTP to WebSocket. 80 | %% @see Yaws 81 | handle_open(WSState, S) -> 82 | Response = ?RES_CONN_ESTABLISHED(pb), 83 | ResBin = message_pb:encode_message(Response), 84 | yaws_websockets:send(WSState, {binary, iolist_to_binary(ResBin)}), 85 | {ok, S}. 86 | 87 | %% @doc This function is called when a binary message is received. 88 | %% {binary, Data} is the unfragmented binary message. 89 | %% SUPPORTED by this handler. 90 | %% @see Yaws 91 | handle_message({binary, Msg}, 92 | #state{nb_bins = M, server = ?WEST{key = K}} = S) -> 93 | ?LOG_INFO("Received binary msg (M=~p): ~p bytes~n", [M, byte_size(Msg)]), 94 | try 95 | DecMsg = message_pb:decode_message(Msg), 96 | Cmd = west_util:to_atom(DecMsg#msg_t.event), 97 | ?LOG_INFO( 98 | "[~p] ~p ~p~n", [K, DecMsg#msg_t.event, DecMsg#msg_t.channel]), 99 | case west_protocol_handler:handle_event(Cmd, DecMsg, S#state.server) of 100 | {ok, Res} -> 101 | BinRes = iolist_to_binary(message_pb:encode_message(Res)), 102 | {reply, {binary, BinRes}, S#state{nb_bins = M + 1}}; 103 | {error, Err} -> 104 | BinErr = iolist_to_binary(message_pb:encode_message(Err)), 105 | {reply, {binary, BinErr}, S#state{nb_bins = M + 1}}; 106 | _ -> 107 | Err1 = ?RES_ACTION_NOT_ALLOWED( 108 | DecMsg#msg_t.id, DecMsg#msg_t.channel, pb), 109 | BinErr1 = iolist_to_binary(message_pb:encode_message(Err1)), 110 | {reply, {binary, BinErr1}, S#state{nb_bins = M + 1}} 111 | end 112 | catch 113 | _:_ -> 114 | Err2 = ?RES_BAD_REQUEST(pb), 115 | BinErr2 = iolist_to_binary(message_pb:encode_message(Err2)), 116 | {reply, {binary, BinErr2}, S#state{nb_bins = M + 1}} 117 | end; 118 | 119 | %% @doc This function is called when a TEXT message is received. 120 | %% NOT HANDLED by this handler. 121 | %% @see Yaws 122 | handle_message({text, Msg}, #state{nb_texts = N} = S) -> 123 | ?LOG_INFO("Received text msg (N=~p): ~p bytes~n", [N, byte_size(Msg)]), 124 | {reply, {text, <<"bad_encoding">>}, S#state{nb_bins = N + 1}}; 125 | 126 | %% @doc When the client closes the connection, the callback module is 127 | %% notified with the message {close, Status, Reason} 128 | %% @see Yaws 129 | handle_message({close, Status, Reason}, _) -> 130 | ?LOG_INFO("Close connection: ~p - ~p~n", [Status, Reason]), 131 | {close, Status}. 132 | 133 | %% @doc 134 | %% If defined, this function is called when a timeout occurs or when 135 | %% the handling process receives any unknown message. 136 | %%
137 | %% Info is either the atom timeout, if a timeout has occurred, or 138 | %% the received message. 139 | %% @see Yaws 140 | handle_info(timeout, S) -> 141 | ?LOG_INFO("process timed out~n", []), 142 | Bin = iolist_to_binary(message_pb:encode_message(?MSG{event = "timeout"})), 143 | {reply, {binary, Bin}, S}; 144 | handle_info(_Info, State) -> 145 | {noreply, State}. 146 | 147 | %% @doc This function is called when the handling process is about to 148 | %% terminate. it should be the opposite of Module:init/1 and do 149 | %% any necessary cleaning up. 150 | %% @see Yaws 151 | terminate(Reason, S) -> 152 | ?LOG_INFO("terminate ~p: ~p (state:~p)~n", [self(), Reason, S]), 153 | ok. 154 | 155 | %%%=================================================================== 156 | %%% Callback 157 | %%%=================================================================== 158 | 159 | %% @private 160 | %% @doc Event callback. This function is executed when message arrives. 161 | ev_callback({ETag, Event, Msg}, [WSRef, Id]) -> 162 | Reply = ?RES_CH_NEW_MSG(Id, ETag, atom_to_binary(Event, utf8), Msg, pb), 163 | BinReply = iolist_to_binary(message_pb:encode_message(Reply)), 164 | yaws_api:websocket_send(WSRef, {binary, BinReply}). 165 | -------------------------------------------------------------------------------- /src/endpoints/yaws/west_yaws_ws_text_handler.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Text Wire Protocol. This module is the `yaws' extended 25 | %%% callback module. Here the WS messages are received and 26 | %%% handled. 27 | %%% @see Yaws Sources 28 | %%% @end 29 | %%% Created : 03. Oct 2013 9:57 AM 30 | %%%------------------------------------------------------------------- 31 | -module(west_yaws_ws_text_handler). 32 | 33 | %% Export for websocket callbacks 34 | -export([init/1, 35 | terminate/2, 36 | handle_open/2, 37 | handle_message/2, 38 | handle_info/2]). 39 | 40 | %% Callback 41 | -export([ev_callback/2]). 42 | 43 | -include("west.hrl"). 44 | 45 | -record(state, {server = ?WEST{}, nb_texts = 0, nb_bins = 0}). 46 | 47 | %%%=================================================================== 48 | %%% WS callback 49 | %%%=================================================================== 50 | 51 | %% @doc Initialize the internal state of the callback module. 52 | %% @see Yaws 53 | init([Arg, InitialState]) -> 54 | ?LOG_INFO("Initalize ~p: ~p~n", [self(), InitialState]), 55 | Dist = application:get_env(west, dist, gproc), 56 | Scope = ?GPROC_SCOPE(Dist), 57 | DistProps = application:get_env(west, dist_props, [{opts, [{n, 1}, {q, 1}]}]), 58 | case string:tokens(yaws_api:arg_pathinfo(Arg), "/") of 59 | [_, Key] -> 60 | Name = west_util:build_name([Key, self(), west_util:get_timestamp_ms()]), 61 | register(Name, self()), 62 | CbSpec = {?MODULE, ev_callback, [{Name, node()}, undefined]}, 63 | {ok, #state{server = ?WEST{name = Name, 64 | key = Key, 65 | dist = Dist, 66 | dist_props = DistProps, 67 | scope = Scope, 68 | cb = CbSpec, 69 | encoding = text}}}; 70 | _ -> 71 | {error, <<"Error, missing key in path.">>} 72 | end. 73 | 74 | %% @doc This function is called when the connection is upgraded from 75 | %% HTTP to WebSocket. 76 | %% @see Yaws 77 | handle_open(WSState, State) -> 78 | yaws_websockets:send(WSState, {text, <<"Welcome !">>}), 79 | {ok, State}. 80 | 81 | %% @doc This function is called when a message <<"bye">> is received. 82 | %% @see Yaws 83 | handle_message({text, <<"bye">>}, #state{nb_texts = N, nb_bins = M} = State) -> 84 | ?LOG_INFO("bye - Msg processed: ~p text, ~p binary~n", [N, M]), 85 | NbTexts = list_to_binary(integer_to_list(N)), 86 | NbBins = list_to_binary(integer_to_list(M)), 87 | Messages = [{text, <<"Goodbye !">>}, 88 | {text, <>}, 89 | {text, <>}], 90 | {close, {1000, <<"bye">>}, Messages, State}; 91 | 92 | %% @doc This function is called when a message <<"ping">> is received. 93 | %% @see Yaws 94 | handle_message({text, <<"ping">>}, #state{nb_texts = N} = State) -> 95 | ?LOG_INFO("Received text msg (N=~p): 4 bytes~n", [N]), 96 | {reply, {text, <<"west pong">>}, State#state{nb_texts = N + 1}}; 97 | 98 | %% @doc This function is called when a TEXT message is received. 99 | %% @see Yaws 100 | handle_message({text, Msg}, #state{nb_texts = N} = State) -> 101 | ?LOG_INFO("Received text msg (N=~p): ~p bytes~n", [N, byte_size(Msg)]), 102 | case dec_msg(Msg) of 103 | none -> 104 | {reply, {text, Msg}, State#state{nb_texts = N + 1}}; 105 | Cmd -> 106 | case handle_event(string:to_lower(Cmd), State#state.server) of 107 | {ok, Reason} -> 108 | {reply, {text, Reason}, State#state{nb_texts = N + 1}}; 109 | {error, Err0} -> 110 | {reply, {text, Err0}, State#state{nb_texts = N + 1}}; 111 | _ -> 112 | ErrMsg = <<"west:action_not_allowed">>, 113 | {reply, {text, ErrMsg}, State#state{nb_texts = N + 1}} 114 | end 115 | end; 116 | 117 | %% @doc This function is called when a binary message is received. 118 | %% NOT HANDLED by this handler. 119 | %% @see Yaws 120 | handle_message({binary, Msg}, #state{nb_bins = M} = State) -> 121 | ?LOG_INFO("Received binary msg (M=~p): ~p bytes~n", [M, byte_size(Msg)]), 122 | {reply, {binary, <<"bad_encoding">>}, State#state{nb_bins = M + 1}}; 123 | 124 | %% @doc When the client closes the connection, the callback module is 125 | %% notified with the message {close, Status, Reason} 126 | %% @see Yaws 127 | handle_message({close, Status, Reason}, _) -> 128 | ?LOG_INFO("Close connection: ~p - ~p~n", [Status, Reason]), 129 | {close, Status}. 130 | 131 | %% @doc 132 | %% If defined, this function is called when a timeout occurs or when 133 | %% the handling process receives any unknown message. 134 | %%
135 | %% Info is either the atom timeout, if a timeout has occurred, or 136 | %% the received message. 137 | %% @see Yaws 138 | handle_info(timeout, State) -> 139 | ?LOG_INFO("process timed out~n", []), 140 | {reply, {text, <<"Anybody Else ?">>}, State}; 141 | handle_info(_Info, State) -> 142 | {noreply, State}. 143 | 144 | %% @doc This function is called when the handling process is about to 145 | %% terminate. it should be the opposite of Module:init/1 and do 146 | %% any necessary cleaning up. 147 | %% @see Yaws 148 | terminate(Reason, State) -> 149 | ?LOG_INFO("terminate ~p: ~p (state:~p)~n", [self(), Reason, State]), 150 | ok. 151 | 152 | %%%=================================================================== 153 | %%% Event handlers 154 | %%%=================================================================== 155 | 156 | %% @private 157 | %% @doc Handle the register event. 158 | handle_event(["reg", Ch], WS) -> 159 | MsgSpec = ?MSG{id = undefined, channel = Ch}, 160 | Res = west_protocol_handler:handle_event(register, MsgSpec, WS), 161 | {ok, bin_msg(Ch, Res)}; 162 | 163 | %% @private 164 | %% @doc Handle the unregister event. 165 | handle_event(["unreg", Ch], WS) -> 166 | MsgSpec = ?MSG{id = undefined, channel = Ch}, 167 | Res = west_protocol_handler:handle_event(unregister, MsgSpec, WS), 168 | {ok, bin_msg(Ch, Res)}; 169 | 170 | %% @private 171 | %% @doc Handle the send event. 172 | handle_event(["send", Ch, Msg], WS) -> 173 | MsgSpec = ?MSG{id = undefined, channel = Ch, data = Msg}, 174 | Res = west_protocol_handler:handle_event(send, MsgSpec, WS), 175 | {ok, bin_msg(Ch, Res)}; 176 | 177 | %% @private 178 | %% @doc Handle the publish event. 179 | handle_event(["pub", Ch, Msg], WS) -> 180 | MsgSpec = ?MSG{id = undefined, channel = Ch, data = Msg}, 181 | Res = west_protocol_handler:handle_event(publish, MsgSpec, WS), 182 | {ok, bin_msg(Ch, Res)}; 183 | 184 | %% @private 185 | %% @doc Handle the subscribe event. 186 | handle_event(["sub", Ch], WS) -> 187 | MsgSpec = ?MSG{id = undefined, channel = Ch}, 188 | Res = west_protocol_handler:handle_event(subscribe, MsgSpec, WS), 189 | {ok, bin_msg(Ch, Res)}; 190 | 191 | %% @private 192 | %% @doc Handle the unsubscribe event. 193 | handle_event(["unsub", Ch], WS) -> 194 | MsgSpec = ?MSG{id = undefined, channel = Ch}, 195 | Res = west_protocol_handler:handle_event(unsubscribe, MsgSpec, WS), 196 | {ok, bin_msg(Ch, Res)}; 197 | 198 | %% @private 199 | handle_event(Any, _State) -> 200 | {none, Any}. 201 | 202 | %%%=================================================================== 203 | %%% Internal functions 204 | %%%=================================================================== 205 | 206 | %% @private 207 | %% @doc Parse the text-based event. 208 | dec_msg(Msg) -> 209 | L = [string:strip(X, both, $ ) || X <- string:tokens(binary_to_list(Msg), "\"")], 210 | case L of 211 | [C, M] -> string:tokens(C, " ") ++ [M]; 212 | [C] -> string:tokens(C, " "); 213 | _ -> none 214 | end. 215 | 216 | %% @private 217 | bin_msg(Ch, {_, ?MSG{event = Event}}) -> 218 | <<(<<"west ">>)/binary, 219 | (iolist_to_binary(Ch ++ ":"))/binary, 220 | (iolist_to_binary(Event))/binary>>. 221 | 222 | %%%=================================================================== 223 | %%% Callback 224 | %%%=================================================================== 225 | 226 | %% @private 227 | %% @doc Event callback. This function is executed when message arrives. 228 | ev_callback({ETag, Event, Msg}, [WSRef, _Id]) -> 229 | Body = case Msg of 230 | Msg when is_binary(Msg) -> 231 | binary_to_list(Msg); 232 | _ -> 233 | Msg 234 | end, 235 | Reply = <<(iolist_to_binary(ETag))/binary, 236 | (<<" ">>)/binary, 237 | (atom_to_binary(Event, utf8))/binary, 238 | (iolist_to_binary(":new_message "))/binary, 239 | (iolist_to_binary(Body))/binary>>, 240 | yaws_api:websocket_send(WSRef, {text, Reply}). 241 | -------------------------------------------------------------------------------- /src/protos/message.proto: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------- 2 | ** 3 | ** Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | ** 5 | ** This file is provided to you under the Apache License, 6 | ** Version 2.0 (the "License"); you may not use this file 7 | ** except in compliance with the License. You may obtain 8 | ** a copy of the License at 9 | ** 10 | ** http://www.apache.org/licenses/LICENSE-2.0 11 | ** 12 | ** Unless required by applicable law or agreed to in writing, 13 | ** software distributed under the License is distributed on an 14 | ** "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | ** KIND, either express or implied. See the License for the 16 | ** specific language governing permissions and limitations 17 | ** under the License. 18 | ** 19 | ** ------------------------------------------------------------------- 20 | */ 21 | 22 | 23 | // Message specification 24 | message Message { 25 | optional string id = 4; 26 | optional string from = 3; 27 | required string event = 1; 28 | optional string channel = 2; 29 | optional bytes data = 5; 30 | } 31 | -------------------------------------------------------------------------------- /src/west.app.src: -------------------------------------------------------------------------------- 1 | {application, west, 2 | [ 3 | {description, "west"}, 4 | {vsn, "0.3.0"}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib, crypto, gproc]}, 7 | {mod, {west_app, []}}, 8 | {env, []} 9 | ] 10 | }. 11 | -------------------------------------------------------------------------------- /src/west.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc This gen_server is a native Erlang WEST client or wrapper. 25 | %%% @end 26 | %%% Created : 10. Nov 2013 10:53 AM 27 | %%%------------------------------------------------------------------- 28 | -module(west). 29 | 30 | -behaviour(gen_server). 31 | 32 | %% API 33 | -export([start_link/3, stop/1, 34 | reg/2, unreg/2, send/3, 35 | sub/2, unsub/2, pub/3]). 36 | 37 | %% gen_server callbacks 38 | -export([init/1, 39 | handle_call/3, 40 | handle_cast/2, 41 | handle_info/2, 42 | terminate/2, 43 | code_change/3]). 44 | 45 | -include("west.hrl"). 46 | 47 | -define(SERVER, ?MODULE). 48 | 49 | -record(state, {server=?WEST{}, opts}). 50 | 51 | -type channel() :: string(). 52 | 53 | %%%=================================================================== 54 | %%% API 55 | %%%=================================================================== 56 | 57 | %% @doc Starts the gen_server. 58 | -spec(start_link(iolist(), cb_spec(), proplist()) -> {ok, pid()} | ignore | {error, term()}). 59 | start_link(Key, CallbackSpec, Opts) -> 60 | gen_server:start_link(?MODULE, [Key, CallbackSpec, Opts], []). 61 | 62 | %% @doc Stops the gen_server. 63 | -spec stop(server_ref()) -> ok. 64 | stop(ServerRef) -> 65 | gen_server:cast(ServerRef, stop). 66 | 67 | %% @doc Register to a point-to-point channel with name `Channel'. All 68 | %% incoming events to the channel `Key' will be handle them by the 69 | %% callback spec. 70 | -spec reg(server_ref(), channel()) -> {ok | error, msg_spec()}. 71 | reg(ServerRef, Channel) -> 72 | gen_server:call(ServerRef, {reg, Channel}). 73 | 74 | %% @doc Unregister from a point-to-point channel with name `Channel'. 75 | -spec unreg(server_ref(), channel()) -> {ok | error, msg_spec()}. 76 | unreg(ServerRef, Channel) -> 77 | gen_server:call(ServerRef, {unreg, Channel}). 78 | 79 | %% @doc Send the message `Msg' to point-to-point channel `Channel'. 80 | %% Just one consumer will receive this message. 81 | -spec send(server_ref(), channel(), msg()) -> {ok | error, msg_spec()}. 82 | send(ServerRef, Channel, Msg) -> 83 | gen_server:call(ServerRef, {send, Channel, Msg}). 84 | 85 | %% @doc Subscribe to a pub/sub channel `Channel'. All incoming events 86 | %% to the channel will be handle them by the callback spec. 87 | -spec sub(server_ref(), channel()) -> {ok | error, msg_spec()}. 88 | sub(ServerRef, Channel) -> 89 | gen_server:call(ServerRef, {sub, Channel}). 90 | 91 | %% @doc Removes a subscription from a pub/sub channel `Channel'. 92 | -spec unsub(server_ref(), channel()) -> {ok | error, msg_spec()}. 93 | unsub(ServerRef, Channel) -> 94 | gen_server:call(ServerRef, {unsub, Channel}). 95 | 96 | %% @doc Publish the message `Msg' to all subscribers to a pub/sub 97 | %% channel `Channel'. 98 | -spec pub(server_ref(), channel(), msg()) -> {ok | error, msg_spec()}. 99 | pub(ServerRef, Channel, Msg) -> 100 | gen_server:call(ServerRef, {pub, Channel, Msg}). 101 | 102 | %%%=================================================================== 103 | %%% gen_server callbacks 104 | %%%=================================================================== 105 | 106 | %% @private 107 | init([Key, CallbackSpec, Opts]) -> 108 | Dist = application:get_env(west, dist, gproc), 109 | Scope = ?GPROC_SCOPE(Dist), 110 | DistProps = application:get_env(west, dist_props, [{opts, [{n, 1}, {q, 1}]}]), 111 | Name = west_util:build_name([Key, self(), west_util:get_timestamp_ms()]), 112 | register(Name, self()), 113 | Server = ?WEST{name = Name, 114 | key = Key, 115 | dist = Dist, 116 | dist_props = DistProps, 117 | scope = Scope, 118 | cb = CallbackSpec}, 119 | {ok, #state{server = Server, opts = Opts}}. 120 | 121 | %% @private 122 | handle_call({reg, Channel}, _From, #state{server = WS} = S) -> 123 | MsgSpec = ?MSG{id = undefined, channel = Channel}, 124 | Reply = west_protocol_handler:handle_event(register, MsgSpec, WS), 125 | {reply, Reply, S}; 126 | 127 | %% @private 128 | handle_call({unreg, Channel}, _From, #state{server = WS} = S) -> 129 | MsgSpec = ?MSG{id = undefined, channel = Channel}, 130 | Reply = west_protocol_handler:handle_event(unregister, MsgSpec, WS), 131 | {reply, Reply, S}; 132 | 133 | %% @private 134 | handle_call({send, Channel, Msg}, _From, #state{server = WS} = S) -> 135 | MsgSpec = ?MSG{id = undefined, channel = Channel, data = Msg}, 136 | Reply = west_protocol_handler:handle_event(send, MsgSpec, WS), 137 | {reply, Reply, S}; 138 | 139 | %% @private 140 | handle_call({sub, Channel}, _From, #state{server = WS} = S) -> 141 | MsgSpec = ?MSG{id = undefined, channel = Channel}, 142 | Reply = west_protocol_handler:handle_event(subscribe, MsgSpec, WS), 143 | {reply, Reply, S}; 144 | 145 | %% @private 146 | handle_call({unsub, Channel}, _From, #state{server = WS} = S) -> 147 | MsgSpec = ?MSG{id = undefined, channel = Channel}, 148 | Reply = west_protocol_handler:handle_event(unsubscribe, MsgSpec, WS), 149 | {reply, Reply, S}; 150 | 151 | %% @private 152 | handle_call({pub, Channel, Msg}, _From, #state{server = WS} = S) -> 153 | MsgSpec = ?MSG{id = undefined, channel = Channel, data = Msg}, 154 | Reply = west_protocol_handler:handle_event(publish, MsgSpec, WS), 155 | {reply, Reply, S}. 156 | 157 | %% @private 158 | handle_cast(stop, State) -> 159 | {stop, normal, State}; 160 | 161 | %% @private 162 | handle_cast(_Request, State) -> 163 | {noreply, State}. 164 | 165 | %% @private 166 | handle_info(_Info, State) -> 167 | {noreply, State}. 168 | 169 | %% @private 170 | terminate(_Reason, _State) -> 171 | ok. 172 | 173 | %% @private 174 | code_change(_OldVsn, State, _Extra) -> 175 | {ok, State}. 176 | -------------------------------------------------------------------------------- /src/west_app.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc This is the application module of `west'. 25 | %%% @end 26 | %%% Created : 03. Oct 2013 9:57 AM 27 | %%%------------------------------------------------------------------- 28 | -module(west_app). 29 | 30 | -behaviour(application). 31 | 32 | %% application 33 | -export([start/0, start/2, stop/1]). 34 | 35 | -define(C_ACCEPTORS, 100). 36 | 37 | %%%=================================================================== 38 | %%% API 39 | %%%=================================================================== 40 | 41 | start() -> 42 | start(normal, []). 43 | 44 | start(_Type, _Args) -> 45 | case west_sup:start_link() of 46 | {ok, Sup} -> 47 | start_riak_core(), 48 | start_web_server(), 49 | {ok, Sup}; 50 | Other -> 51 | {error, Other} 52 | end. 53 | 54 | stop(_State) -> 55 | ok. 56 | 57 | %%%=================================================================== 58 | %%% Internal functions 59 | %%%=================================================================== 60 | 61 | start_riak_core() -> 62 | case application:get_env(west, dist) of 63 | {ok, west_dist} -> 64 | west_util:start_app_deps(riak_core), 65 | ok = riak_core:register(west, [{vnode_module, west_dist_vnode}]), 66 | %%ok = riak_core_ring_events:add_guarded_handler(west_ring_event_handler, []), 67 | %%ok = riak_core_node_watcher_events:add_guarded_handler(west_node_event_handler, []), 68 | ok = riak_core_node_watcher:service_up(west, self()); 69 | _ -> 70 | ok 71 | end. 72 | 73 | start_web_server() -> 74 | case application:get_env(west, web_server) of 75 | {ok, yaws} -> 76 | start_yaws(); 77 | {ok, cowboy} -> 78 | start_cowboy(); 79 | _ -> 80 | ok 81 | end. 82 | 83 | start_yaws() -> 84 | case application:get_all_env(yaws) of 85 | [] -> 86 | ok; 87 | Yaws -> 88 | Id = "embedded", 89 | Gconf = proplists:get_value(gconf, Yaws, gconf()), 90 | Sconf = proplists:get_value(sconf, Yaws, sconf()), 91 | Docroot = proplists:get_value(docroot, Sconf, "./www"), 92 | ok = yaws:start_embedded(Docroot, Sconf, Gconf, Id) 93 | end. 94 | 95 | gconf() -> 96 | [{id, "embedded"}, {ebin_dir, ["./ebin"]}, {runmod, "yapp"}]. 97 | 98 | sconf() -> 99 | [{servername, "west_server"}, 100 | {listen, {127, 0, 0, 1}}, 101 | {port, 8080}, 102 | {docroot, "./www"}, 103 | {appmods, [{"websocket", west_ws_endpoint}]}, 104 | {opaque, [{yapp_server_id, "yapp_west"}, 105 | {bootstrap_yapps, "west"}]}]. 106 | 107 | start_cowboy() -> 108 | case application:get_all_env(cowboy) of 109 | [] -> 110 | ok; 111 | _ -> 112 | west_util:start_app_deps(cowboy), 113 | Routes = application:get_env(cowboy, routes, cowboy_routes()), 114 | Dispatch = cowboy_router:compile(Routes), 115 | TransOpts = application:get_env(cowboy, trans_opts, [{port, 8080}]), 116 | ProtoOpts = [{env, [{dispatch, Dispatch}]}], 117 | Cacceptors = application:get_env(cowboy, c_acceptors, ?C_ACCEPTORS), 118 | {ok, _} = cowboy:start_http(http, Cacceptors, TransOpts, ProtoOpts) 119 | end. 120 | 121 | cowboy_routes() -> 122 | [ 123 | {'_', [ 124 | {"/", cowboy_static, {file, "./www/index.html"}}, 125 | {"/websocket/text/:key", west_ws_text_protocol_handler, []}, 126 | {"/websocket/json/:key", west_ws_json_protocol_handler, []}, 127 | {"/websocket/pb/:key", west_ws_pb_protocol_handler, []}, 128 | {"/[...]", cowboy_static, {dir, "./www", [{mimetypes, cow_mimetypes, all}]}} 129 | ] 130 | } 131 | ]. 132 | -------------------------------------------------------------------------------- /src/west_dist.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Interface into the WEST distributed application. 25 | %%% @see 26 | %%% @end 27 | %%% Created : 04. Nov 2013 8:47 PM 28 | %%%------------------------------------------------------------------- 29 | -module(west_dist). 30 | 31 | %% API 32 | -export([ping/0, cmd/3, cmd/4]). 33 | 34 | %% Debug 35 | -export([get_dbg_preflist/2, get_dbg_preflist/3]). 36 | 37 | -include("west.hrl"). 38 | 39 | -define(TIMEOUT, 5000). 40 | 41 | %%%=================================================================== 42 | %%% API 43 | %%%=================================================================== 44 | 45 | %% @doc Pings a random vnode to make sure communication is functional. 46 | %% @spec ping() -> term() 47 | ping() -> 48 | DocIdx = riak_core_util:chash_key({<<"ping">>, term_to_binary(now())}), 49 | PrefList = riak_core_apl:get_primary_apl(DocIdx, 1, west), 50 | [{IdxNode, _Type}] = PrefList, 51 | riak_core_vnode_master:sync_spawn_command(IdxNode, ping, west_dist_vnode_master). 52 | 53 | %% @doc 54 | %% Executes the command given in the `Val' spec. 55 | %% @equiv cmd(Bucket, Key, Val, []) 56 | cmd(Bucket, Key, Val) -> 57 | cmd(Bucket, Key, Val, []). 58 | 59 | %% @doc 60 | %% Same as previous but it can receive option list. 61 | %%
  • Bucket: Bucket to calculate hash (Riak Core). 62 | %%
  • Key: Key to calculate hash (Riak Core). 63 | %%
  • Val: Value that will be received by VNode. 64 | %%
  • 65 | %% Opts: Option list. 66 | %% q = quorum 67 | %% n = replicas 68 | %% Example: [{q, 1}, {n, 1}] 69 | %%
  • 70 | %% 71 | %% @spec cmd(Bucket, Key, Val, Opts) -> 72 | %% {Res, ReqID} | {Res, ReqID, Reason} | {error, timeout} 73 | %% Bucket = binary() 74 | %% Key = binary() 75 | %% Val = {Ref, Key, CbSpec} 76 | %% Opts = proplist() 77 | cmd(Bucket, Key, Val, Opts) -> 78 | do_write(Bucket, Key, cmd, Val, Opts). 79 | 80 | %% @doc 81 | %% Gets the preflist with default number of nodes (replicas). 82 | %% @equiv get_dbg_preflist(Bucket, Key, ?N) 83 | get_dbg_preflist(Bucket, Key) -> 84 | get_dbg_preflist(Bucket, Key, ?N). 85 | 86 | %% @doc 87 | %% Same as previous but it can receive the number of replicas (nodes). 88 | %%
  • Bucket: Bucket to calculate hash (Riak Core).
  • 89 | %%
  • Key: Key to calculate hash (Riak Core).
  • 90 | %%
  • N: Number of replicas.
  • 91 | %% 92 | %% @spec get_dbg_preflist(Bucket, Key, N) -> term() 93 | %% Bucket = binary() 94 | %% Key = binary() 95 | %% N = non_neg_integer() 96 | get_dbg_preflist(Bucket, Key, N) -> 97 | DocIdx = riak_core_util:chash_key({iolist_to_binary(Bucket), 98 | iolist_to_binary(Key)}), 99 | riak_core_apl:get_apl(DocIdx, N, west). 100 | 101 | %%%=================================================================== 102 | %%% Internal Functions 103 | %%%=================================================================== 104 | 105 | %% @private 106 | %% @doc Execute the command against the FSM. 107 | do_write(Bucket, Key, Op, Val, Opts) -> 108 | BBucket = iolist_to_binary(Bucket), 109 | BKey = iolist_to_binary(Key), 110 | {ok, ReqID} = west_dist_cmd_fsm:cmd(BBucket, BKey, Op, Val, Opts), 111 | wait_for_reqid(ReqID, ?TIMEOUT). 112 | 113 | %% @private 114 | %% @doc Waits for the FMS response. 115 | wait_for_reqid(ReqID, Timeout) -> 116 | receive 117 | {Code, ReqID} -> 118 | Code; 119 | {_Code, ReqID, Reply} -> 120 | case is_list(Reply) of 121 | true -> 122 | case lists:keyfind(ok, 1, Reply) of 123 | {_, V} -> V; 124 | _ -> lists:last(Reply) 125 | end; 126 | _ -> 127 | Reply 128 | end 129 | after Timeout -> 130 | {error, timeout} 131 | end. 132 | -------------------------------------------------------------------------------- /src/west_dist_cmd_fsm.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc The coordinator for command opeartions. 25 | %%% @see 26 | %%% @end 27 | %%% Created : 04. Nov 2013 8:47 PM 28 | %%%------------------------------------------------------------------- 29 | -module(west_dist_cmd_fsm). 30 | 31 | -behavior(gen_fsm). 32 | 33 | -include("west.hrl"). 34 | 35 | %% API 36 | -export([start_link/5, start_link/7, 37 | cmd/3, cmd/5]). 38 | 39 | %% Callbacks 40 | -export([init/1, 41 | code_change/4, 42 | handle_event/3, 43 | handle_info/3, 44 | handle_sync_event/4, 45 | terminate/3]). 46 | 47 | %% States 48 | -export([prepare/2, execute/2, waiting/2]). 49 | 50 | %% State 51 | -record(state, {req_id :: pos_integer(), 52 | from :: pid(), 53 | bkey :: binary(), 54 | op :: atom(), 55 | val = undefined :: term() | undefined, 56 | preflist :: riak_core_apl:preflist2(), 57 | n :: non_neg_integer(), 58 | num_q = 0 :: non_neg_integer(), 59 | q = ?W :: non_neg_integer(), 60 | replies = [] :: list()}). 61 | 62 | %%%=================================================================== 63 | %%% API 64 | %%%=================================================================== 65 | 66 | %% @doc start_link/5. 67 | %% Creates a gen_fsm process which calls Module:init/1 to 68 | %% initialize. To ensure a synchronized start-up procedure, this 69 | %% function does not return until Module:init/1 has returned. 70 | %%
    71 | %%
  • ReqID: The request id so the caller can verify the response.
  • 72 | %%
  • From: The pid of the sender so a reply can be made.
  • 73 | %%
  • Bucket: Bucket to calculate hash.
  • 74 | %%
  • Key: Key to calculate hash.
  • 75 | %%
  • Op: The cmd op, one of reg|unreg|send|sub|unsub|pub
  • 76 | %% 77 | %% @spec start_link(ReqID, From, Bucket, Key, Op) -> 78 | %% {ok, pid()} | ignore | {error, Reason :: term()} 79 | %% ReqID = pos_integer() 80 | %% From = pid() 81 | %% Bucket = binary() 82 | %% Key = binary() 83 | %% Op = atom() 84 | %% 85 | %% @equiv start_link(ReqID, From, Bucket, Key, Op, undefined, []) 86 | start_link(ReqID, From, Bucket, Key, Op) -> 87 | start_link(ReqID, From, Bucket, Key, Op, undefined, []). 88 | 89 | %% @doc start_link/7. 90 | %% Same of the previous function but, it can receive value and 91 | %% option list. 92 | %%
    93 | %%
  • ReqID: The request id so the caller can verify the response.
  • 94 | %%
  • From: The pid of the sender so a reply can be made.
  • 95 | %%
  • Bucket: Bucket to calculate hash.
  • 96 | %%
  • Key: Key to calculate hash.
  • 97 | %%
  • Op: The cmd op, one of reg|unreg|send|sub|unsub|pub
  • 98 | %%
  • Val: Any value that is passed to FSM.
  • 99 | %%
  • 100 | %% Opts: Option list. 101 | %% q = quorum 102 | %% n = replicas 103 | %% Example: [{q, 1}, {n, 1}] 104 | %%
  • 105 | %% 106 | %% @spec start_link(ReqID, From, Bucket, Key, Op, Val, Opts) -> 107 | %% {ok, pid()} | ignore | {error, Reason :: term()} 108 | %% ReqID = pos_integer() 109 | %% From = pid() 110 | %% Bucket = binary() 111 | %% Key = binary() 112 | %% Op = atom() 113 | %% Val = any() 114 | %% Opts = proplist() 115 | start_link(ReqID, From, Bucket, Key, Op, Val, Opts) -> 116 | gen_fsm:start_link(?MODULE, [ReqID, From, Bucket, Key, Op, Val, Opts], []). 117 | 118 | %% @doc cmd/3. 119 | %% Start the FSM in order to execute the given command. 120 | %%
  • Bucket: Bucket to calculate hash.
  • 121 | %%
  • Key: Key to calculate hash.
  • 122 | %%
  • Op: The cmd op, one of reg|unreg|send|sub|unsub|pub
  • 123 | %% 124 | %% @spec cmd(Bucket, Key, Op) -> 125 | %% {ok, pid()} | ignore | {error, Reason :: term()} 126 | %% Bucket = binary() 127 | %% Key = binary() 128 | %% Op = atom() 129 | %% 130 | %% @equiv cmd(Bucket, Key, Op, undefined, []) 131 | cmd(Bucket, Key, Op) -> 132 | cmd(Bucket, Key, Op, undefined, []). 133 | 134 | %% @doc cmd/5. 135 | %% Same of previous but with value and option list. 136 | %%
  • Bucket: Bucket to calculate hash.
  • 137 | %%
  • Key: Key to calculate hash.
  • 138 | %%
  • Op: The cmd op, one of reg|unreg|send|sub|unsub|pub
  • 139 | %%
  • Val: Any value that is passed to FSM.
  • 140 | %%
  • 141 | %% Opts: Option list. 142 | %% q = quorum 143 | %% n = replicas 144 | %% Example: [{q, 1}, {n, 1}] 145 | %%
  • 146 | %% 147 | %% @spec cmd(Bucket, Key, Op, Val, Opts) -> 148 | %% {ok, pid()} | ignore | {error, Reason :: term()} 149 | %% Bucket = binary() 150 | %% Key = binary() 151 | %% Op = atom() 152 | %% Val = any() 153 | %% Opts = proplist() 154 | cmd(Bucket, Key, Op, Val, Opts) -> 155 | ReqID = mk_reqid(), 156 | west_dist_cmd_fsm_sup:start_cmd_fsm( 157 | [ReqID, self(), Bucket, Key, Op, Val, Opts]), 158 | {ok, ReqID}. 159 | 160 | %%%=================================================================== 161 | %%% States 162 | %%%=================================================================== 163 | 164 | %% @private 165 | init([ReqID, From, Bucket, Key, Op, Val, Opts]) -> 166 | Q = west_util:keyfind(q, Opts, ?W), 167 | N = west_util:keyfind(n, Opts, ?N), 168 | SD = #state{req_id = ReqID, 169 | from = From, 170 | bkey = {Bucket, Key}, 171 | op = Op, 172 | val = Val, 173 | n = N, 174 | q = Q}, 175 | {ok, prepare, SD, 0}. 176 | 177 | %% @private 178 | %% @doc Prepare the write by calculating the _preference list_. 179 | prepare(timeout, SD0 = #state{bkey = BKey, n = N}) -> 180 | DocIdx = riak_core_util:chash_key(BKey), 181 | PrefList = riak_core_apl:get_apl(DocIdx, N, west), 182 | SD = SD0#state{preflist = PrefList}, 183 | {next_state, execute, SD, 0}. 184 | 185 | %% @private 186 | %% @doc Execute the command request and then go into waiting state to 187 | %% verify it has meets consistency requirements. 188 | execute(timeout, 189 | SD0 = #state{req_id = ReqID, op = Op, val = Val, preflist = PrefL}) -> 190 | case Val of 191 | undefined -> 192 | west_dist_vnode:Op(PrefL, ReqID); 193 | _ -> 194 | west_dist_vnode:Op(PrefL, ReqID, Val) 195 | end, 196 | {next_state, waiting, SD0}. 197 | 198 | %% @private 199 | %% @doc Wait for Q write reqs to respond. 200 | waiting({_Res, ReqID, Val}, 201 | SD0 = #state{from = From, num_q = NumQ0, q = Q, replies = Replies0}) -> 202 | NumQ = NumQ0 + 1, 203 | Replies = [Val | Replies0], 204 | SD = SD0#state{num_q = NumQ, replies = Replies}, 205 | case NumQ =:= Q of 206 | true -> 207 | Reply = case lists:any(different(Val), Replies) of 208 | true -> Replies; 209 | false -> Val 210 | end, 211 | From ! {ok, ReqID, Reply}, 212 | {stop, normal, SD}; 213 | false -> 214 | {next_state, waiting, SD} 215 | end. 216 | 217 | %% @private 218 | %% @doc If any event info is received, FSM finish. 219 | handle_info(_Info, _StateName, StateData) -> 220 | {stop, badmsg, StateData}. 221 | 222 | %% @private 223 | %% @doc If any event is received, FSM finish. 224 | handle_event(_Event, _StateName, StateData) -> 225 | {stop, badmsg, StateData}. 226 | 227 | %% @private 228 | %% @doc If any event is received, FSM finish. 229 | handle_sync_event(_Event, _From, _StateName, StateData) -> 230 | {stop, badmsg, StateData}. 231 | 232 | %% @private 233 | %% @doc Convert process state when code is changed. 234 | code_change(_OldVsn, StateName, State, _Extra) -> 235 | {ok, StateName, State}. 236 | 237 | %% @private 238 | terminate(_Reason, _SN, _SD) -> 239 | ok. 240 | 241 | %%%=================================================================== 242 | %%% Internal Functions 243 | %%%=================================================================== 244 | 245 | %% @private 246 | mk_reqid() -> erlang:phash2(os:timestamp()). 247 | 248 | %% @private 249 | different(A) -> fun(B) -> A =/= B end. 250 | -------------------------------------------------------------------------------- /src/west_dist_cmd_fsm_sup.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Supervise the west_dist_cmd FSM. 25 | %%% @see 26 | %%% @end 27 | %%% Created : 04. Nov 2013 8:47 PM 28 | %%%------------------------------------------------------------------- 29 | -module(west_dist_cmd_fsm_sup). 30 | 31 | -behavior(supervisor). 32 | 33 | %% API 34 | -export([start_cmd_fsm/1, start_link/0]). 35 | 36 | %% Supervisor callbacks 37 | -export([init/1]). 38 | 39 | %%%=================================================================== 40 | %%% API functions 41 | %%%=================================================================== 42 | 43 | %% @doc Starts a new child (worker). 44 | %% @spec start_cmd_fsm(Args :: list()) -> supervisor:startchild_ret() 45 | start_cmd_fsm(Args) -> 46 | supervisor:start_child(?MODULE, Args). 47 | 48 | %% @doc Starts the supervisor 49 | %% @spec start_link() -> supervisor:startlink_ret() 50 | start_link() -> 51 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 52 | 53 | %%%=================================================================== 54 | %%% Supervisor callbacks 55 | %%%=================================================================== 56 | 57 | %% @private 58 | init([]) -> 59 | CmdFsm = {west_dist_cmd_fsm, 60 | {west_dist_cmd_fsm, start_link, []}, 61 | temporary, 62 | 5000, 63 | worker, 64 | [west_dist_cmd_fsm]}, 65 | {ok, {{simple_one_for_one, 10, 10}, [CmdFsm]}}. 66 | -------------------------------------------------------------------------------- /src/west_dist_console.erl: -------------------------------------------------------------------------------- 1 | %% @doc Interface for west-admin commands. 2 | -module(west_dist_console). 3 | 4 | -export([join/1, 5 | leave/1, 6 | remove/1, 7 | ringready/1]). 8 | 9 | join([NodeStr]) -> 10 | try 11 | case riak_core:join(NodeStr) of 12 | ok -> 13 | io:format("Sent join request to ~s\n", [NodeStr]), 14 | ok; 15 | {error, not_reachable} -> 16 | io:format("Node ~s is not reachable!\n", [NodeStr]), 17 | error; 18 | {error, different_ring_sizes} -> 19 | io:format("Failed: ~s has a different ring_creation_size~n", 20 | [NodeStr]), 21 | error 22 | end 23 | catch 24 | Exception:Reason -> 25 | lager:error("Join failed ~p:~p", [Exception, Reason]), 26 | io:format("Join failed, see log for details~n"), 27 | error 28 | end. 29 | 30 | leave([]) -> 31 | remove_node(node()). 32 | 33 | remove([Node]) -> 34 | remove_node(list_to_atom(Node)). 35 | 36 | remove_node(Node) when is_atom(Node) -> 37 | try 38 | case catch (riak_core:remove_from_cluster(Node)) of 39 | {'EXIT', {badarg, [{erlang, hd, [[]]} | _]}} -> 40 | %% This is a workaround because 41 | %% riak_core_gossip:remove_from_cluster doesn't check if 42 | %% the result of subtracting the current node from the 43 | %% cluster member list results in the empty list. When 44 | %% that code gets refactored this can probably go away. 45 | io:format("Leave failed, this node is the only member.~n"), 46 | error; 47 | Res -> 48 | io:format(" ~p\n", [Res]) 49 | end 50 | catch 51 | Exception:Reason -> 52 | lager:error("Leave failed ~p:~p", [Exception, Reason]), 53 | io:format("Leave failed, see log for details~n"), 54 | error 55 | end. 56 | 57 | 58 | -spec(ringready([]) -> ok | error). 59 | ringready([]) -> 60 | try 61 | case riak_core_status:ringready() of 62 | {ok, Nodes} -> 63 | io:format("TRUE All nodes agree on the ring ~p~n", [Nodes]); 64 | {error, {different_owners, N1, N2}} -> 65 | io:format("FALSE Node ~p and ~p list different partition owners~n", 66 | [N1, N2]), 67 | error; 68 | {error, {nodes_down, Down}} -> 69 | io:format("FALSE ~p down. All nodes need to be up to check.~n", 70 | [Down]), 71 | error 72 | end 73 | catch 74 | Exception:Reason -> 75 | lager:error("Ringready failed ~p:~p", [Exception, Reason]), 76 | io:format("Ringready failed, see log for details~n"), 77 | error 78 | end. 79 | -------------------------------------------------------------------------------- /src/west_dist_vnode.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc A vnode to handle commands for WEST. The vnode requests will 25 | %%% be hashed on Bucket and Key and will use a coordinator to 26 | %%% enforce N/R/W values. 27 | %%% @see 28 | %%% @end 29 | %%% Created : 04. Nov 2013 8:47 PM 30 | %%%------------------------------------------------------------------- 31 | -module(west_dist_vnode). 32 | 33 | -behaviour(riak_core_vnode). 34 | 35 | %% riak_core_vnode API 36 | -export([start_vnode/1, 37 | init/1, 38 | terminate/2, 39 | handle_command/3, 40 | is_empty/1, 41 | delete/1, 42 | handle_handoff_command/3, 43 | handoff_starting/2, 44 | handoff_cancelled/1, 45 | handoff_finished/2, 46 | handle_handoff_data/2, 47 | encode_handoff_item/2, 48 | handle_coverage/4, 49 | handle_exit/3]). 50 | 51 | %% API 52 | -export([cmd/3]). 53 | 54 | %% VNode State 55 | -record(state, {partition, machine_id, last_timestamp}). 56 | 57 | %% Master vnode name 58 | -define(MASTER, west_dist_vnode_master). 59 | 60 | %% Sync Call 61 | -define(sync(PrefList, Command, Master), 62 | riak_core_vnode_master:sync_command(PrefList, Command, Master) 63 | ). 64 | 65 | %%%=================================================================== 66 | %%% API 67 | %%%=================================================================== 68 | 69 | %% @doc Starts the vnode. 70 | %% @spec start_vnode(I :: any()) -> Reply :: term() 71 | start_vnode(I) -> 72 | riak_core_vnode_master:get_vnode_pid(I, ?MODULE). 73 | 74 | %% @doc cmd/3. 75 | %% Execute the given command applying the callback function. This 76 | %% callback can be an operation like: 77 | %% reg|unreg|send|sub|unsub|pub 78 | %%
    79 | %%
  • PrefList: Riak Core preflist.
  • 80 | %%
  • ReqID: Request id so the caller can verify the response.
  • 81 | %%
  • Ref: Unique reference to the GS that will be created.
  • 82 | %%
  • Key: Key which the GS will be registered.
  • 83 | %%
  • CallbackSpec: Callback specification. This will applied when 84 | %% messages arrives. If `Mod' is `none', the callback will be treated 85 | %% as a fun.
  • 86 | %% 87 | %% @spec cmd(PrefList, ReqID, CallbackSpec) -> Reply :: term() 88 | %% PrefList = any() 89 | %% ReqID = Hash :: integer() >= 0 90 | %% CallbackSpec = {Mod :: atom(), Fun :: atom(), Args :: list()} 91 | cmd(PrefList, ReqID, CallbackSpec) -> 92 | riak_core_vnode_master:command( 93 | PrefList, 94 | {ReqID, CallbackSpec}, 95 | {fsm, undefined, self()}, 96 | ?MASTER). 97 | 98 | %%%=================================================================== 99 | %%% VNode Callbacks 100 | %%%=================================================================== 101 | 102 | %% @private 103 | init([Partition]) -> 104 | TS = os:timestamp(), 105 | %% This could get ugly if you expect them to be unique across data 106 | %% centers, or if you have more than 1024 partitions 107 | <> = <>, 108 | {ok, #state{partition = Partition, machine_id = MachineID, last_timestamp = TS}}. 109 | 110 | %% @private 111 | %% @doc Handle ping command - for verification purposes. 112 | handle_command(ping, _Sender, State) -> 113 | {reply, {pong, State#state.partition}, State}; 114 | 115 | %% @private 116 | %% @doc Handle received command. Applies the callback function. 117 | handle_command({ReqID, {M, F, A}}, _Sender, State) -> 118 | Reply = case M of 119 | none when is_function(F) -> 120 | apply(F, A); 121 | _ -> 122 | apply(M, F, A) 123 | end, 124 | {reply, {ok, ReqID, Reply}, State}. 125 | 126 | %% @private 127 | %% @doc Not handled. 128 | handle_handoff_command(_Message, _Sender, State) -> 129 | %% Delay a little to naively avoid ID collisions 130 | timer:sleep(1000), 131 | {forward, State}. 132 | 133 | %% @private 134 | %% @doc Not handled. 135 | handoff_starting(_TargetNode, _State) -> 136 | {true, _State}. 137 | 138 | %% @private 139 | %% @doc Not handled. 140 | handoff_cancelled(State) -> 141 | {ok, State}. 142 | 143 | %% @private 144 | %% @doc Not handled. 145 | handoff_finished(_TargetNode, State) -> 146 | {ok, State}. 147 | 148 | %% @private 149 | %% @doc Not handled. 150 | handle_handoff_data(_Data, State) -> 151 | {reply, ok, State}. 152 | 153 | %% @private 154 | %% @doc Not handled. 155 | encode_handoff_item(_ObjectName, _ObjectValue) -> 156 | <<>>. 157 | 158 | %% @private 159 | %% @doc Not handled. 160 | is_empty(State) -> 161 | {true, State}. 162 | 163 | %% @private 164 | %% @doc Not handled. 165 | delete(State) -> 166 | {ok, State}. 167 | 168 | %% @private 169 | %% @doc Not handled. 170 | handle_coverage(_Req, _KeySpaces, _Sender, State) -> 171 | {stop, not_implemented, State}. 172 | 173 | %% @private 174 | %% @doc Not handled. 175 | handle_exit(_Pid, Reason, State) -> 176 | {stop, Reason, State}. 177 | 178 | %% @private 179 | %% @doc Not handled. 180 | terminate(_Reason, _State) -> 181 | ok. 182 | -------------------------------------------------------------------------------- /src/west_event_handler.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc `WEST'. This module handles subscription's lifecycle. It 25 | %%% receives events published by other processes from other nodes 26 | %%% and when events arrives, a callback func is applied. 27 | %%% @end 28 | %%% Created : 03. Oct 2013 9:57 AM 29 | %%%------------------------------------------------------------------- 30 | -module(west_event_handler). 31 | 32 | -behaviour(gen_server). 33 | 34 | %% API 35 | -export([start_link/3, create/3, 36 | reg/2, unreg/2, send/4, 37 | subscribe/2, unsubscribe/2, publish/4, 38 | set_callback/2, 39 | get_events/1, 40 | delete/1]). 41 | 42 | %% Callback 43 | -export([init/1, 44 | handle_call/3, 45 | handle_cast/2, 46 | handle_info/2, 47 | terminate/2, 48 | code_change/3]). 49 | 50 | %% Includes 51 | -include("west.hrl"). 52 | -include("west_int.hrl"). 53 | 54 | -define(SERVER, ?MODULE). 55 | 56 | -record(state, {scope, events = [], cb = ?CALLBACK{}}). 57 | 58 | %%%=================================================================== 59 | %%% API 60 | %%%=================================================================== 61 | 62 | %% @doc Start a gen_server to register the subscription and handle 63 | %% events. 64 | -spec start_link(scope(), cb_spec(), proplist()) -> {ok, pid()} | {error, term()}. 65 | start_link(Scope, CallbackSpec, Opts) -> 66 | gen_server:start_link(?MODULE, [Scope, CallbackSpec, Opts], []). 67 | 68 | %% @doc Create a message-driven handler to handle incoming messages. 69 | %% This new process is added to the supervisor's tree. 70 | -spec create(scope(), cb_spec(), proplist()) -> supervisor:startchild_ret(). 71 | create(Scope, CallbackSpec, Opts) -> 72 | west_event_handler_sup:start_child(Scope, CallbackSpec, Opts). 73 | 74 | %% @doc Register to a point-to-point channel with name `Key'. 75 | %% All incoming events to the channel `Key' will be handle them 76 | %% by this process. 77 | -spec reg(server_ref(), key()) -> {ok, key()} | {error, term()}. 78 | reg(ServerRef, Key) -> 79 | gen_server:call(ServerRef, {reg, Key}). 80 | 81 | %% @doc Unregister from a point-to-point channel with name `Key'. This 82 | %% process won't handle incoming events to the channel any more. 83 | -spec unreg(server_ref(), key()) -> {ok, key()} | {error, term()}. 84 | unreg(ServerRef, Key) -> 85 | gen_server:call(ServerRef, {unreg, Key}). 86 | 87 | %% @doc Send the message `Msg' to point-to-point channel `Key'. 88 | %% Just one consumer will receive this message. 89 | -spec send(server_ref(), any(), key(), msg()) -> {ok, key()} | {error, term()}. 90 | send(ServerRef, Id, Key, Msg) -> 91 | gen_server:call(ServerRef, {send, Id, Key, Msg}). 92 | 93 | %% @doc Subscribe to a pub/sub channel `Event'. All incoming events 94 | %% to the channel `Event' will be handle them by this process. 95 | -spec subscribe(server_ref(), event()) -> {ok, event()} | {error, term()}. 96 | subscribe(ServerRef, Event) -> 97 | gen_server:call(ServerRef, {subscribe, Event}). 98 | 99 | %% @doc Delete a subscription from a pub/sub channel `Event'. This 100 | %% process won't handle incoming events to channel `Event' 101 | %% any more. 102 | -spec unsubscribe(server_ref(), event()) -> {ok, event()} | {error, term()}. 103 | unsubscribe(ServerRef, Event) -> 104 | gen_server:call(ServerRef, {unsubscribe, Event}). 105 | 106 | %% @doc Publish the message `Msg' to all subscribers to a pub/sub 107 | %% channel `Event'. 108 | -spec publish(server_ref(), any(), event(), msg()) -> ok. 109 | publish(ServerRef, ETag, Event, Msg) -> 110 | gen_server:cast(ServerRef, {publish, ETag, Event, Msg}). 111 | 112 | %% @doc Sets the callback spec and returns the previous one. 113 | -spec set_callback(server_ref(), cb_spec()) -> {ok, cb_spec()} | {error, term()}. 114 | set_callback(ServerRef, CallbackSpec) -> 115 | gen_server:call(ServerRef, {set_callback, CallbackSpec}). 116 | 117 | %% @doc Get the events which this server is subscribed. 118 | -spec get_events(server_ref()) -> {ok, list()} | {error, term()}. 119 | get_events(ServerRef) -> 120 | gen_server:call(ServerRef, get_events). 121 | 122 | %% @doc Stops this gen_server. 123 | -spec delete(server_ref()) -> ok. 124 | delete(ServerRef) -> 125 | gen_server:cast(ServerRef, delete). 126 | 127 | %%%=================================================================== 128 | %%% gen_server callbacks 129 | %%%=================================================================== 130 | 131 | %% @private 132 | init([Scope, CallbackSpec, Opts]) -> 133 | Monitors = west_util:keyfind(monitors, Opts, []), 134 | lists:foreach(fun(I) -> erlang:monitor(process, I) end, Monitors), 135 | {M, F, A} = CallbackSpec, 136 | {ok, #state{scope = Scope, cb = ?CALLBACK{mod = M, func = F, args = A}}}. 137 | 138 | %% @private 139 | %% @doc Handles the register command. 140 | handle_call({reg, Key}, _From, #state{scope = Scope} = S) -> 141 | case ?REG(Scope, Key) of 142 | true -> {reply, {ok, Key}, S}; 143 | {error, Reason} -> {reply, {error, Reason}, S}; 144 | _ -> {reply, {error, unexpected}, S} 145 | end; 146 | 147 | %% @private 148 | %% @doc Handles the unregister command. 149 | handle_call({unreg, Key}, _From, #state{scope = Scope} = S) -> 150 | case ?UNREG(Scope, Key) of 151 | true -> {reply, {ok, Key}, S}; 152 | {error, Reason} -> {reply, {error, Reason}, S}; 153 | _ -> {reply, {error, unexpected}, S} 154 | end; 155 | 156 | %% @private 157 | %% @doc Handles the send command. 158 | handle_call({send, Id, Key, Msg}, _From, #state{scope = Scope} = S) -> 159 | case ?SEND(Scope, Id, Key, Msg) of 160 | true -> {reply, {ok, Key}, S}; 161 | {error, Reason} -> {reply, {error, Reason}, S}; 162 | _ -> {reply, {error, unexpected}, S} 163 | end; 164 | 165 | %% @private 166 | %% @doc Handles the subscribe command. 167 | handle_call({subscribe, Event}, 168 | _From, 169 | #state{scope = Scope, events = EvL, cb = Cb} = S) -> 170 | case ?PS_SUB(Scope, Event) of 171 | true -> 172 | NewS = #state{scope = Scope, events = [Event | EvL], cb = Cb}, 173 | {reply, {ok, Event}, NewS}; 174 | {error, Reason} -> 175 | {reply, {error, Reason}, S}; 176 | _ -> 177 | {reply, {error, unexpected}, S} 178 | end; 179 | 180 | %% @private 181 | %% @doc Handles the unsubscribe command. 182 | handle_call({unsubscribe, Event}, 183 | _From, 184 | #state{scope = Scope, events = EvL, cb = Cb} = S) -> 185 | case ?PS_UNSUB(Scope, Event) of 186 | true -> 187 | NewS = #state{scope = Scope, events = lists:delete(Event, EvL), cb = Cb}, 188 | {reply, {ok, Event}, NewS}; 189 | {error, Reason} -> 190 | {reply, {error, Reason}, S}; 191 | _ -> 192 | {reply, {error, unexpected}, S} 193 | end; 194 | 195 | %% @private 196 | %% @doc Handles the set_callback command. 197 | handle_call({set_callback, {M, F, A}}, 198 | _From, 199 | #state{scope = Scope, events = EvL, cb = Cb}) -> 200 | CbSpec = ?CALLBACK{mod = M, func = F, args = A}, 201 | {reply, {ok, Cb}, #state{scope = Scope, events = EvL, cb = CbSpec}}; 202 | 203 | %% @private 204 | %% @doc Handles the get_events command. 205 | handle_call(get_events, _From, #state{events = EvL} = S) -> 206 | {reply, {ok, EvL}, S}; 207 | 208 | %% @private 209 | %% @doc Unhandled. 210 | handle_call(_, _, S) -> 211 | {reply, badarg, S}. 212 | 213 | %% @private 214 | %% @doc Handles the publish command. 215 | handle_cast({publish, ETag, Event, Msg}, #state{scope = Scope} = S) -> 216 | ?PS_PUB(Scope, ETag, Event, Msg), 217 | {noreply, S}; 218 | 219 | %% @private 220 | %% @doc Handles the delete command. 221 | handle_cast(delete, S) -> 222 | {stop, normal, S}; 223 | 224 | %% @private 225 | %% @doc Unhandled. 226 | handle_cast(_Msg, S) -> 227 | {noreply, S}. 228 | 229 | %% @private 230 | %% @doc Handle incoming events. 231 | %% This callback handle the incoming events from the registered event. 232 | %% When a message arrives to this `EvKey' the callback functions which 233 | %% this server was created `#state{cb=#callback{}}' is applied. 234 | handle_info({ETag, EvKey, Msg}, 235 | #state{cb = ?CALLBACK{mod = M, func = F, args = A}} = S) -> 236 | case M of 237 | none when is_function(F) -> 238 | apply(F, [{ETag, EvKey, Msg}, A]); 239 | _ -> 240 | apply(M, F, [{ETag, EvKey, Msg}, A]) 241 | end, 242 | {noreply, S}; 243 | 244 | %% @private 245 | %% @doc Monitor has benn triggered, stop this gen_server. 246 | handle_info({'DOWN', _Ref, process, Pid, _Reason}, S) -> 247 | error_logger:info_msg( 248 | "gen_server ~p stopped, monitor of ~p triggered.", [self(), Pid]), 249 | {stop, normal, S}; 250 | 251 | %% @private 252 | %% @doc Unhandled. 253 | handle_info(_, S) -> 254 | {noreply, S}. 255 | 256 | %% @private 257 | %% @doc Finish the gen_server. 258 | terminate(_Reason, _State) -> 259 | ok. 260 | 261 | %% @private 262 | %% @doc Unhandled. 263 | code_change(_OldVsn, State, _Extra) -> 264 | {ok, State}. 265 | -------------------------------------------------------------------------------- /src/west_event_handler_sup.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Supervisor of 'west_event_handler'. 25 | %%% @end 26 | %%% Created : 03. Oct 2013 9:57 AM 27 | %%%------------------------------------------------------------------- 28 | -module(west_event_handler_sup). 29 | 30 | -behaviour(supervisor). 31 | 32 | %% API 33 | -export([start_link/0, start_child/3]). 34 | 35 | %% Callback 36 | -export([init/1]). 37 | 38 | -include("west.hrl"). 39 | 40 | -define(SERVER, ?MODULE). 41 | 42 | %%%=================================================================== 43 | %%% API 44 | %%%=================================================================== 45 | 46 | %% @doc Start a gen_server to register the subscription and handle 47 | %% incoming events to this subscription. 48 | -spec start_link() -> supervisor:startlink_ret(). 49 | start_link() -> 50 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 51 | 52 | %% @doc Starts a new child `west_event_handler'. 53 | -spec start_child(scope(), cb_spec(), proplist()) -> supervisor:startchild_ret(). 54 | start_child(Scope, CallbackSpec, Opts) -> 55 | supervisor:start_child(?SERVER, [Scope, CallbackSpec, Opts]). 56 | 57 | %%%=================================================================== 58 | %%% Supervisor callbacks 59 | %%%=================================================================== 60 | 61 | %% @private 62 | init(_Args) -> 63 | Element = {west_event_handler, 64 | {west_event_handler, start_link, []}, 65 | transient, 66 | brutal_kill, 67 | worker, 68 | [west_event_handler]}, 69 | Children = [Element], 70 | RestartStrategy = {simple_one_for_one, 10, 60}, 71 | {ok, {RestartStrategy, Children}}. 72 | -------------------------------------------------------------------------------- /src/west_int.hrl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Shared internal functions. 25 | %%% @end 26 | %%% Created : 03. Oct 2013 9:57 AM 27 | %%%------------------------------------------------------------------- 28 | 29 | -define(CATCH_ERROR(Expr), 30 | try Expr 31 | catch _:Error -> {error, Error} 32 | end 33 | ). 34 | 35 | -define(THROW_ERROR(E), throw({error, E})). 36 | 37 | -define(PS_SUB(Scope, Event), 38 | try 39 | gproc_ps:subscribe(Scope, Event) 40 | catch 41 | _:_ -> {error, gproc_sub_failed} 42 | end 43 | ). 44 | 45 | -define(PS_SUB_EXT(Pid, Scope, Event), 46 | try 47 | GPKey = {p, Scope, {gproc_ps_event, Event}}, 48 | case gproc_lib:insert_reg(GPKey, gproc:default(GPKey), Pid, Scope) of 49 | false -> 50 | {error, badarg}; 51 | true -> 52 | erlang:monitor(process, Pid), 53 | true 54 | end 55 | catch 56 | _:_ -> {error, gproc_sub_failed} 57 | end 58 | ). 59 | 60 | -define(PS_UNSUB(Scope, Event), 61 | try 62 | gproc_ps:unsubscribe(Scope, Event) 63 | catch 64 | _:_ -> {error, gproc_unsub_failed} 65 | end 66 | ). 67 | 68 | -define(PS_UNSUB_EXT(Pid, Scope, Event), 69 | try 70 | GPKey = {p, Scope, {gproc_ps_event, Event}}, 71 | gproc_lib:remove_reg(GPKey, Pid, unreg), 72 | true 73 | catch 74 | _:_ -> {error, gproc_unsub_failed} 75 | end 76 | ). 77 | 78 | -define(PS_PUB(Scope, ETag, Event, Msg), 79 | try 80 | gproc:send({p, Scope, {gproc_ps_event, Event}}, {ETag, Event, Msg}), 81 | true 82 | catch 83 | _:_ -> {error, gproc_pub_failed} 84 | end 85 | ). 86 | 87 | -define(PS_PUB_ALL(Scope, ETag, Event, Msg), 88 | try 89 | rpc:multicall( 90 | gproc, send, [{p, Scope, {gproc_ps_event, Event}}, {ETag, Event, Msg}]), 91 | true 92 | catch 93 | _:_ -> {error, gproc_pub_failed} 94 | end 95 | ). 96 | 97 | -define(REG(Scope, Key), 98 | try 99 | gproc:reg({n, Scope, Key}) 100 | catch 101 | _:_ -> {error, gproc_reg_failed} 102 | end 103 | ). 104 | 105 | -define(REG_EXT(Pid, Scope, Key), 106 | try 107 | GPKey = {n, Scope, Key}, 108 | case gproc_lib:insert_reg(GPKey, gproc:default(GPKey), Pid, Scope) of 109 | false -> 110 | {error, badarg}; 111 | true -> 112 | erlang:monitor(process, Pid), 113 | true 114 | end 115 | catch 116 | _:_ -> {error, gproc_reg_failed} 117 | end 118 | ). 119 | 120 | -define(UNREG(Scope, Key), 121 | try 122 | gproc:unreg({n, Scope, Key}) 123 | catch 124 | _:_ -> {error, gproc_unreg_failed} 125 | end 126 | ). 127 | 128 | -define(UNREG_EXT(Pid, Scope, Key), 129 | try 130 | GPKey = {n, Scope, Key}, 131 | gproc_lib:remove_reg(GPKey, Pid, unreg), 132 | true 133 | catch 134 | _:_ -> {error, gproc_unreg_failed} 135 | end 136 | ). 137 | 138 | -define(SEND(Scope, ETag, Key, Msg), 139 | try 140 | gproc:send({n, Scope, Key}, {ETag, Key, Msg}), 141 | true 142 | catch 143 | _:_ -> {error, gproc_send_failed} 144 | end 145 | ). 146 | 147 | -define(WHERE(Scope, Key), 148 | try 149 | gproc:where({n, Scope, Key}) 150 | catch 151 | _:_ -> {error, gproc_where_failed} 152 | end 153 | ). 154 | 155 | -define(PROC_INFO(Pid), 156 | try 157 | {gproc, Value} = gproc:info(Pid, gproc), 158 | Value 159 | catch 160 | _:_ -> {error, gproc_info_failed} 161 | end 162 | ). 163 | 164 | -define(PROC_TYPE(Pid), 165 | try 166 | {gproc, [{{Type, _, _}, _} | _T]} = gproc:info(Pid, gproc), 167 | Type 168 | catch 169 | _:_ -> {error, gproc_info_type_failed} 170 | end 171 | ). 172 | 173 | -define(ENC_JSON(JsonTerm), west_util:enc_json(JsonTerm)). 174 | 175 | -define(DEC_JSON(JsonData), west_util:dec_json(JsonData)). 176 | -------------------------------------------------------------------------------- /src/west_lib.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Interface into the WEST distributed application. 25 | %%% @see 26 | %%% @end 27 | %%% Created : 05. Nov 2013 12:12 PM 28 | %%%------------------------------------------------------------------- 29 | -module(west_lib). 30 | 31 | %% API 32 | -export([reg/4, unreg/2, send/4, 33 | sub/4, unsub/2, pub/4]). 34 | 35 | -include("west_int.hrl"). 36 | 37 | %%%=================================================================== 38 | %%% API 39 | %%%=================================================================== 40 | 41 | %% @doc reg/4. 42 | %% Register to a point-to-point channel with name `Key'. All incoming 43 | %% events to the channel `Key' will be handle them by a GS created 44 | %% by this process. 45 | %%
    46 | %% Creates a GS event handler and register it into Gproc, locally. 47 | %% This GS will handle the incoming messages. 48 | %%
    49 | %%
  • Scope: Gproc scope.
  • 50 | %%
  • Ref: Unique reference to the GS that will be created.
  • 51 | %%
  • Key: Key which the GS will be registered.
  • 52 | %%
  • CbSpec: Callback specification. This will be called when messages 53 | %% arrives.
  • 54 | %% 55 | %% @spec reg(Scope, Ref, Key, CbSpec) -> Reply :: term() 56 | %% Scope = atom() 57 | %% Ref = any() 58 | %% Key = atom() 59 | %% CbSpec = {Mod :: atom(), Fun :: atom(), Args :: list()} 60 | reg(Scope, Ref, Key, CbSpec) -> 61 | Name = west_util:build_name([Ref, Key]), 62 | case whereis(Name) of 63 | undefined -> 64 | {ok, Pid} = west_event_handler:create(Scope, CbSpec, [{monitors, [Ref]}]), 65 | register(Name, Pid), 66 | case west_event_handler:reg(Name, Key) of 67 | {ok, _} -> 68 | {ok, registration_succeeded, Key}; 69 | {error, _} -> 70 | west_event_handler:delete(Name), 71 | {error, registration_denied, Key} 72 | end; 73 | _ -> 74 | {error, registration_already_exist, Key} 75 | end. 76 | 77 | %% @doc unreg/2. 78 | %% Unregister from a point-to-point channel with name `Key'. The 79 | %% created GS process won't handle incoming events to channel `Key' 80 | %% any more. 81 | %%
    82 | %% Destroy the GS event handler in order to delete the registration 83 | %% from Gproc. 84 | %%
    85 | %%
  • Ref: Unique reference to the GS that will be created.
  • 86 | %%
  • Key: Key which the GS was registered.
  • 87 | %% 88 | %% @spec unreg(Ref :: any(), Key :: atom()) -> Reply :: term() 89 | unreg(Ref, Key) -> 90 | Name = west_util:build_name([Ref, Key]), 91 | case whereis(Name) of 92 | undefined -> 93 | {error, registration_not_found, Key}; 94 | Pid -> 95 | case ?PROC_TYPE(Pid) of 96 | n -> 97 | west_event_handler:delete(Name), 98 | {ok, unregistration_succeeded, Key}; 99 | _ -> 100 | {error, registration_not_found, Key} 101 | end 102 | end. 103 | 104 | %% @doc send/4. 105 | %% Send the message `Msg' to point-to-point channel `Key'. Just one 106 | %% consumer will receive this message. 107 | %%
    108 | %% Sends the given message `Msg' to a `Key'. If the registration to 109 | %% `Key' exist, message will be received by the registered GS. If 110 | %% registration doesn't exist, send will fail. 111 | %%
    112 | %%
  • Scope: Gproc scope.
  • 113 | %%
  • ETag: ID of the sender.
  • 114 | %%
  • Key: Key which the GS was registered.
  • 115 | %%
  • Msg: Message that will send.
  • 116 | %% 117 | %% @spec send(Scope, ETag, Key, Msg) -> Reply :: term() 118 | %% Scope = atom() 119 | %% ETag = string() 120 | %% Key = atom() 121 | %% Msg = binary() | list() 122 | send(Scope, ETag, Key, Msg) -> 123 | F = fun() -> 124 | case ?SEND(Scope, ETag, Key, Msg) of 125 | true -> 126 | {ok, sending_succeeded, Key}; 127 | _ -> 128 | {error, sending_failed, Key} 129 | end 130 | end, 131 | case Scope of 132 | g -> 133 | case ?WHERE(Scope, Key) of 134 | undefined -> 135 | {error, sending_failed, Key}; 136 | {error, _} -> 137 | {error, sending_failed, Key}; 138 | _ -> 139 | F() 140 | end; 141 | _ -> 142 | F() 143 | end. 144 | 145 | %% @doc sub/4. 146 | %% Subscribe to a pub/sub channel `Event'. All incoming events to the 147 | %% channel `Event' will be handle them by GS created by this process. 148 | %%
    149 | %% Creates a GS event handler and subscribe it into Gproc, in order 150 | %% to handle the subscription lifecycle and handle the published 151 | %% messages to `Event'. 152 | %%
    153 | %%
  • Scope: Gproc scope.
  • 154 | %%
  • Ref: Unique reference to the GS that will be created.
  • 155 | %%
  • Event: Event which the GS will be subscribed.
  • 156 | %%
  • CbSpec: Callback specification. This will be called when messages 157 | %% arrives.
  • 158 | %% 159 | %% @spec sub(Scope, Ref, Event, CbSpec) -> Reply :: term() 160 | %% Scope = atom() 161 | %% Ref = any() 162 | %% Event = atom() 163 | %% CbSpec = {Mod :: atom(), Fun :: atom(), Args :: list()} 164 | sub(Scope, Ref, Event, CbSpec) -> 165 | Name = west_util:build_name([Ref, Event]), 166 | case whereis(Name) of 167 | undefined -> 168 | {ok, Pid} = west_event_handler:create(Scope, CbSpec, [{monitors, [Ref]}]), 169 | register(Name, Pid), 170 | case west_event_handler:subscribe(Name, Event) of 171 | {ok, _} -> 172 | {ok, subscription_succeeded, Event}; 173 | {error, _} -> 174 | west_event_handler:delete(Name), 175 | {error, subscription_failed, Event} 176 | end; 177 | _ -> 178 | {error, subscription_already_exist, Event} 179 | end. 180 | 181 | %% @doc unsub/2. 182 | %% Delete a subscription from a pub/sub channel `Event'.The 183 | %% created GS process won't handle incoming events to channel `Event' 184 | %% any more. 185 | %%
    186 | %% Destroy the GS event handler in order to delete the subscription 187 | %% from Gproc. Ends the subscription lifecycle. 188 | %%
    189 | %%
  • Ref: Unique reference to the GS that will be created.
  • 190 | %%
  • Event: Event which the GS was subscribed.
  • 191 | %% 192 | %% @spec unsub(Ref :: any(), Event :: atom()) -> Reply :: term() 193 | unsub(Ref, Event) -> 194 | Name = west_util:build_name([Ref, Event]), 195 | case whereis(Name) of 196 | undefined -> 197 | {error, subscription_not_found, Event}; 198 | Pid -> 199 | case ?PROC_TYPE(Pid) of 200 | p -> 201 | west_event_handler:delete(Name), 202 | {ok, unsubscription_succeeded, Event}; 203 | _ -> 204 | {error, subscription_not_found, Event} 205 | end 206 | end. 207 | 208 | %% @doc pub/4. 209 | %% Publish the message `Msg' to all subscribers to a pub/sub channel 210 | %% `Event'. 211 | %%
    212 | %% Publishes the given message `Msg' into the a `Event'. If the 213 | %% subscription to `Event' exist, message will be received by the 214 | %% subscribed GS. If subscription doesn't exist, publish will fail. 215 | %%
    216 | %%
  • Scope: Gproc scope.
  • 217 | %%
  • ETag: ID of the sender.
  • 218 | %%
  • Event: Event which the GS was registered.
  • 219 | %%
  • Msg: Message that will send.
  • 220 | %% 221 | %% @spec pub(Scope, ETag, Event, Msg) -> Reply :: term() 222 | %% Scope = atom() 223 | %% ETag = string() 224 | %% Event = atom() 225 | %% Msg = binary() | list() 226 | pub(Scope, ETag, Event, Msg) -> 227 | case ?PS_PUB(Scope, ETag, Event, Msg) of 228 | true -> 229 | {ok, publication_succeeded, Event}; 230 | _ -> 231 | {error, publication_failed, Event} 232 | end. 233 | -------------------------------------------------------------------------------- /src/west_msg.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Common JSON utilities to `WEST'. 25 | %%% @end 26 | %%% Created : 03. Oct 2013 6:13 PM 27 | %%%------------------------------------------------------------------- 28 | -module(west_msg). 29 | 30 | %% API 31 | -export([dec_msg/2, enc_msg/2, build_msg/6]). 32 | 33 | -include("west.hrl"). 34 | -include("west_int.hrl"). 35 | 36 | -type field() :: atom() | binary() | iolist(). 37 | 38 | %%%=================================================================== 39 | %%% API 40 | %%%=================================================================== 41 | 42 | -spec dec_msg(any(), atom()) -> msg_spec(). 43 | dec_msg(Data, Encoding) -> 44 | dec_msg1(Data, Encoding). 45 | 46 | -spec enc_msg(msg_spec(), atom()) -> any(). 47 | enc_msg(Msg, Encoding) -> 48 | enc_msg1(Msg, Encoding). 49 | 50 | -spec build_msg(field(), field(), field(), field(), field(), atom()) -> any(). 51 | build_msg(Id, From, Event, Channel, Data, Encoding) -> 52 | Msg = ?MSG{id = Id, from = From, event = Event, channel = Channel, data = Data}, 53 | enc_msg(Msg, Encoding). 54 | 55 | %%%=================================================================== 56 | %%% Internals 57 | %%%=================================================================== 58 | 59 | %% @private 60 | dec_msg1(Data, json) -> 61 | case ?DEC_JSON(Data) of 62 | {error, _} -> 63 | {error, invalid_json}; 64 | {DecJson} -> 65 | try 66 | Id = west_util:keyfind(<<"id">>, DecJson), 67 | From = west_util:keyfind(<<"from">>, DecJson), 68 | Event = west_util:to_atom(west_util:keyfind(<<"event">>, DecJson)), 69 | Ch = west_util:keyfind(<<"channel">>, DecJson), 70 | Data = west_util:keyfind(<<"data">>, DecJson), 71 | ?MSG{event = Event, channel = Ch, from = From, id = Id, data = Data} 72 | catch 73 | _:_ -> {error, decoding_error} 74 | end 75 | end; 76 | dec_msg1(Data, _) -> 77 | Data. 78 | 79 | %% @private 80 | enc_msg1(Msg, json) -> 81 | F = ?record_to_list(msg_t), 82 | ?ENC_JSON({F(Msg)}); 83 | enc_msg1(Msg, _) -> 84 | Msg. 85 | -------------------------------------------------------------------------------- /src/west_protocol.hrl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Protocol Macros. 25 | %%% @end 26 | %%% Created : 06. Oct 2013 8:45 AM 27 | %%%------------------------------------------------------------------- 28 | 29 | -define(RES_CONN_ESTABLISHED(F), 30 | west_msg:build_msg( 31 | undefined, west, connection_established, undefined, undefined, F) 32 | ). 33 | 34 | -define(RES_PONG(Id, F), 35 | west_msg:build_msg( 36 | Id, west, pong, undefined, undefined, F) 37 | ). 38 | 39 | -define(RES_INTERNAL_ERROR(Id, F), 40 | west_msg:build_msg( 41 | Id, west, internal_error, undefined, undefined, F) 42 | ). 43 | 44 | -define(RES_BAD_REQUEST(F), 45 | west_msg:build_msg( 46 | undefined, west, bad_request, undefined, undefined, F) 47 | ). 48 | 49 | -define(RES_ACTION_NOT_ALLOWED(Id, C, F), 50 | west_msg:build_msg( 51 | Id, west, action_not_allowed, C, undefined, F) 52 | ). 53 | 54 | -define(RES_CH_NEW_MSG(Id, ET, C, B, F), 55 | west_msg:build_msg( 56 | Id, ET, new_message, C, B, F) 57 | ). 58 | 59 | -define(RES_CH_NOT_FOUND(Id, C, F), 60 | west_msg:build_msg( 61 | Id, west, channel_not_found, C, undefined, F) 62 | ). 63 | 64 | -define(RES_CH_CREATION_OK(Id, C, F), 65 | west_msg:build_msg( 66 | Id, west, channel_creation_succeeded, C, undefined, F) 67 | ). 68 | 69 | -define(RES_CH_ALREADY_EXIST(Id, C, F), 70 | west_msg:build_msg( 71 | Id, west, channel_already_exist, C, undefined, F) 72 | ). 73 | 74 | -define(RES_CH_CREATION_FAILED(Id, C, F), 75 | west_msg:build_msg( 76 | Id, west, channel_creation_failed, C, undefined, F) 77 | ). 78 | 79 | -define(RES_CH_DELETE_OK(Id, C, F), 80 | west_msg:build_msg( 81 | Id, west, channel_delete_succeeded, C, undefined, F) 82 | ). 83 | 84 | -define(RES_CH_DELETE_FAILED(Id, C, F), 85 | west_msg:build_msg( 86 | Id, west, channel_delete_failed, C, undefined, F) 87 | ). 88 | 89 | -define(RES_REG_OK(Id, C, F), 90 | west_msg:build_msg( 91 | Id, west, registration_succeeded, C, undefined, F) 92 | ). 93 | 94 | -define(RES_REG_FAILED(Id, C, F), 95 | west_msg:build_msg( 96 | Id, west, registration_failed, C, undefined, F) 97 | ). 98 | 99 | -define(RES_REG_ALREADY_EXIST(Id, C, F), 100 | west_msg:build_msg( 101 | Id, west, registration_already_exist, C, undefined, F) 102 | ). 103 | 104 | -define(RES_REG_NOT_FOUND(Id, C, F), 105 | west_msg:build_msg( 106 | Id, west, registration_not_found, C, undefined, F) 107 | ). 108 | 109 | -define(RES_REG_DENIED(Id, C, F), 110 | west_msg:build_msg( 111 | Id, west, registration_denied, C, undefined, F) 112 | ). 113 | 114 | -define(RES_UNREG_OK(Id, C, F), 115 | west_msg:build_msg( 116 | Id, west, unregistration_succeeded, C, undefined, F) 117 | ). 118 | 119 | -define(RES_UNREG_FAILED(Id, C, F), 120 | west_msg:build_msg( 121 | Id, west, unregistration_failed, C, undefined, F) 122 | ). 123 | 124 | -define(RES_SEND_OK(Id, C, F), 125 | west_msg:build_msg( 126 | Id, west, sending_succeeded, C, undefined, F) 127 | ). 128 | 129 | -define(RES_SEND_FAILED(Id, C, F), 130 | west_msg:build_msg( 131 | Id, west, sending_failed, C, undefined, F) 132 | ). 133 | 134 | -define(RES_SUB_OK(Id, C, F), 135 | west_msg:build_msg( 136 | Id, west, subscription_succeeded, C, undefined, F) 137 | ). 138 | 139 | -define(RES_SUB_ALREADY_EXIST(Id, C, F), 140 | west_msg:build_msg( 141 | Id, west, subscription_already_exist, C, undefined, F) 142 | ). 143 | 144 | -define(RES_SUB_NOT_FOUND(Id, C, F), 145 | west_msg:build_msg( 146 | Id, west, subscription_not_found, C, undefined, F) 147 | ). 148 | 149 | -define(RES_SUB_FAILED(Id, C, F), 150 | west_msg:build_msg( 151 | Id, west, subscription_failed, C, undefined, F) 152 | ). 153 | 154 | -define(RES_UNSUB_OK(Id, C, F), 155 | west_msg:build_msg( 156 | Id, west, unsubscription_succeeded, C, undefined, F) 157 | ). 158 | 159 | -define(RES_UNSUB_FAILED(Id, C, F), 160 | west_msg:build_msg( 161 | Id, west, unsubscription_failed, C, undefined, F) 162 | ). 163 | 164 | -define(RES_PUB_OK(Id, C, F), 165 | west_msg:build_msg( 166 | Id, west, publication_succeeded, C, undefined, F) 167 | ). 168 | 169 | -define(RES_PUB_FAILED(Id, C, F), 170 | west_msg:build_msg( 171 | Id, west, publication_failed, C, undefined, F) 172 | ). 173 | -------------------------------------------------------------------------------- /src/west_protocol_handler.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License", F); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc Protocol handler. This module handles the incoming events. 25 | %%% @end 26 | %%% Created : 10. Nov 2013 8:59 AM 27 | %%%------------------------------------------------------------------- 28 | -module(west_protocol_handler). 29 | 30 | %% API 31 | -export([handle_event/3]). 32 | 33 | -include("west.hrl"). 34 | -include("west_int.hrl"). 35 | -include("west_protocol.hrl"). 36 | 37 | %%%=================================================================== 38 | %%% API 39 | %%%=================================================================== 40 | 41 | %% @private 42 | %% @doc Handle the ping event. 43 | handle_event(ping, ?MSG{id = Id}, ?WEST{encoding = F}) -> 44 | {ok, ?RES_PONG(Id, F)}; 45 | 46 | %% @private 47 | %% @doc Handle the register event. 48 | handle_event(register, 49 | ?MSG{id = Id, channel = Ch}, 50 | ?WEST{name = Name, scope = Sc, cb = Cb, encoding = F} = WS) -> 51 | case Ch =/= undefined of 52 | true -> 53 | Val = {west_lib, reg, [Sc, {Name, node()}, Ch, Cb]}, 54 | case execute(WS, Ch, Ch, Val) of 55 | {error, _} -> 56 | {error, ?RES_INTERNAL_ERROR(Id, F)}; 57 | {_, registration_succeeded, _} -> 58 | {ok, ?RES_REG_OK(Id, Ch, F)}; 59 | {_, registration_denied, _} -> 60 | {ok, ?RES_REG_DENIED(Id, Ch, F)}; 61 | {_, registration_already_exist, _} -> 62 | {ok, ?RES_REG_ALREADY_EXIST(Id, Ch, F)}; 63 | _ -> 64 | {error, ?RES_REG_FAILED(Id, Ch, F)} 65 | end; 66 | _ -> 67 | {error, ?RES_REG_FAILED(Id, Ch, F)} 68 | end; 69 | 70 | %% @private 71 | %% @doc Handle the unregister event. 72 | handle_event(unregister, 73 | ?MSG{id = Id, channel = Ch}, 74 | ?WEST{name = Name, encoding = F} = WS) -> 75 | case Ch =/= undefined of 76 | true -> 77 | Val = {west_lib, unreg, [{Name, node()}, Ch]}, 78 | case execute(WS, Ch, Ch, Val) of 79 | {error, _} -> 80 | {error, ?RES_INTERNAL_ERROR(Id, F)}; 81 | {_, unregistration_succeeded, _} -> 82 | {ok, ?RES_UNREG_OK(Id, Ch, F)}; 83 | {_, registration_not_found, _} -> 84 | {ok, ?RES_REG_NOT_FOUND(Id, Ch, F)}; 85 | _ -> 86 | {error, ?RES_UNREG_FAILED(Id, Ch, F)} 87 | end; 88 | _ -> 89 | {error, ?RES_UNREG_FAILED(Id, Ch, F)} 90 | end; 91 | 92 | %% @private 93 | %% @doc Handle the send event. 94 | handle_event(send, 95 | ?MSG{id = Id, channel = Ch, data = Data}, 96 | ?WEST{key = K, scope = Scope, encoding = F} = WS) -> 97 | case Ch =/= undefined andalso Data =/= undefined of 98 | true -> 99 | Val = {west_lib, send, [Scope, K, Ch, Data]}, 100 | case execute(WS, Ch, Ch, Val) of 101 | {error, _} -> 102 | {error, ?RES_INTERNAL_ERROR(Id, F)}; 103 | {_, sending_succeeded, _} -> 104 | {ok, ?RES_SEND_OK(Id, Ch, F)}; 105 | {_, sending_failed, _} -> 106 | {ok, ?RES_REG_NOT_FOUND(Id, Ch, F)}; 107 | _ -> 108 | {error, ?RES_SEND_FAILED(Id, Ch, F)} 109 | end; 110 | _ -> 111 | {error, ?RES_SEND_FAILED(Id, Ch, F)} 112 | end; 113 | 114 | %% @private 115 | %% @doc Handle the subscribe event. 116 | handle_event(subscribe, 117 | ?MSG{id = Id, channel = Ch}, 118 | ?WEST{name = Name, key = K, scope = Sc, cb = Cb, encoding = F} = WS) -> 119 | case Ch =/= undefined of 120 | true -> 121 | Val = {west_lib, sub, [Sc, {Name, node()}, Ch, Cb]}, 122 | case execute(WS, K, Ch, Val) of 123 | {error, _} -> 124 | {error, ?RES_INTERNAL_ERROR(Id, F)}; 125 | {_, subscription_succeeded, _} -> 126 | {ok, ?RES_SUB_OK(Id, Ch, F)}; 127 | {_, subscription_failed, _} -> 128 | {ok, ?RES_SUB_FAILED(Id, Ch, F)}; 129 | {_, subscription_already_exist, _} -> 130 | {ok, ?RES_SUB_ALREADY_EXIST(Id, Ch, F)}; 131 | _ -> 132 | {error, ?RES_SUB_FAILED(Id, Ch, F)} 133 | end; 134 | _ -> 135 | {error, ?RES_SUB_FAILED(Id, Ch, F)} 136 | end; 137 | 138 | %% @private 139 | %% @doc Handle the unsubscribe event. 140 | handle_event(unsubscribe, 141 | ?MSG{id = Id, channel = Ch}, 142 | ?WEST{name = Name, key = K, encoding = F} = WS) -> 143 | case Ch =/= undefined of 144 | true -> 145 | Val = {west_lib, unsub, [{Name, node()}, Ch]}, 146 | case execute(WS, K, Ch, Val) of 147 | {error, _} -> 148 | {error, ?RES_INTERNAL_ERROR(Id, F)}; 149 | {_, unsubscription_succeeded, _} -> 150 | {ok, ?RES_UNSUB_OK(Id, Ch, F)}; 151 | {_, subscription_not_found, _} -> 152 | {ok, ?RES_SUB_NOT_FOUND(Id, Ch, F)}; 153 | _ -> 154 | {error, ?RES_UNSUB_FAILED(Id, Ch, F)} 155 | end; 156 | _ -> 157 | {error, ?RES_UNSUB_FAILED(Id, Ch, F)} 158 | end; 159 | 160 | %% @private 161 | %% @doc Handle the publish event. 162 | handle_event(publish, 163 | ?MSG{id = Id, channel = Ch, data = Data}, 164 | ?WEST{key = K, dist = Dist, encoding = F}) -> 165 | case Ch =/= undefined andalso Data =/= undefined of 166 | true -> 167 | case Dist of 168 | gproc_dist -> 169 | ?PS_PUB(g, K, Ch, Data); 170 | _ -> 171 | ?PS_PUB_ALL(l, K, Ch, Data) 172 | end, 173 | {ok, ?RES_PUB_OK(Id, Ch, F)}; 174 | _ -> 175 | {error, ?RES_PUB_FAILED(Id, Ch, F)} 176 | end; 177 | 178 | %% @private 179 | %% @doc Unhandled events. 180 | handle_event(Any, _Msg, _State) -> 181 | {none, Any}. 182 | 183 | %%%=================================================================== 184 | %%% Internal functions 185 | %%%=================================================================== 186 | 187 | %% @private 188 | %% @doc Executes the asked command. 189 | execute(?WEST{dist = Dist, dist_props = PL}, B, K, {M, F, A} = Val) -> 190 | case Dist of 191 | west_dist -> 192 | Opts = west_util:keyfind(opts, PL, []), 193 | apply(west_dist, cmd, [B, K, Val, Opts]); 194 | _ -> 195 | apply(M, F, A) 196 | end. 197 | -------------------------------------------------------------------------------- /src/west_sup.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2013 Carlos Andres Bolaños, Inc. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | %%%------------------------------------------------------------------- 22 | %%% @author Carlos Andres Bolaños R.A. 23 | %%% @copyright (C) 2013, , All Rights Reserved. 24 | %%% @doc `WEST' supervisor. 25 | %%% @end 26 | %%% Created : 03. Oct 2013 9:57 AM 27 | %%%------------------------------------------------------------------- 28 | -module(west_sup). 29 | 30 | -behaviour(supervisor). 31 | 32 | %% Public API 33 | -export([start_link/0]). 34 | 35 | %% Supervisor callbacks 36 | -export([init/1]). 37 | 38 | %%%=================================================================== 39 | %%% API 40 | %%%=================================================================== 41 | 42 | %% @doc Starts the `WEST' supervisor. 43 | -spec start_link() -> supervisor:startlink_ret(). 44 | start_link() -> 45 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 46 | 47 | %%%=================================================================== 48 | %%% Supervisor Callbacks 49 | %%%=================================================================== 50 | 51 | %% @private 52 | init([]) -> 53 | {ok, {{one_for_one, 5, 10}, process_specs()}}. 54 | 55 | %%%=================================================================== 56 | %%% Internal functions 57 | %%%=================================================================== 58 | 59 | %% @private 60 | %% @doc Build the process specifications that will be supervised. 61 | -spec process_specs() -> [supervisor:child_spec()]. 62 | process_specs() -> 63 | EvHdlr_sup = {west_event_handler_sup, 64 | {west_event_handler_sup, start_link, []}, 65 | permanent, 66 | 2000, 67 | supervisor, 68 | [west_event_handler_sup]}, 69 | Dist = case application:get_env(west, dist) of 70 | {ok, west_dist} -> 71 | VMaster = {west_dist_vnode_master, 72 | {riak_core_vnode_master, start_link, [west_dist_vnode]}, 73 | permanent, 74 | 5000, 75 | worker, 76 | [riak_core_vnode_master]}, 77 | CmdFSM = {west_dist_cmd_fsm_sup, 78 | {west_dist_cmd_fsm_sup, start_link, []}, 79 | permanent, 80 | infinity, 81 | supervisor, 82 | [west_dist_cmd_fsm_sup]}, 83 | [VMaster, CmdFSM]; 84 | _ -> 85 | [] 86 | end, 87 | [EvHdlr_sup] ++ Dist. 88 | -------------------------------------------------------------------------------- /www/assets/css/pygments-manni.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | /*{ background: #f0f3f3; }*/ 3 | .c { color: #999; } /* Comment */ 4 | .err { color: #AA0000; background-color: #FFAAAA } /* Error */ 5 | .k { color: #006699; } /* Keyword */ 6 | .o { color: #555555 } /* Operator */ 7 | .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */ 8 | .cp { color: #009999 } /* Comment.Preproc */ 9 | .c1 { color: #999; } /* Comment.Single */ 10 | .cs { color: #999; } /* Comment.Special */ 11 | .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */ 12 | .ge { font-style: italic } /* Generic.Emph */ 13 | .gr { color: #FF0000 } /* Generic.Error */ 14 | .gh { color: #003300; } /* Generic.Heading */ 15 | .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */ 16 | .go { color: #AAAAAA } /* Generic.Output */ 17 | .gp { color: #000099; } /* Generic.Prompt */ 18 | .gs { } /* Generic.Strong */ 19 | .gu { color: #003300; } /* Generic.Subheading */ 20 | .gt { color: #99CC66 } /* Generic.Traceback */ 21 | .kc { color: #006699; } /* Keyword.Constant */ 22 | .kd { color: #006699; } /* Keyword.Declaration */ 23 | .kn { color: #006699; } /* Keyword.Namespace */ 24 | .kp { color: #006699 } /* Keyword.Pseudo */ 25 | .kr { color: #006699; } /* Keyword.Reserved */ 26 | .kt { color: #007788; } /* Keyword.Type */ 27 | .m { color: #FF6600 } /* Literal.Number */ 28 | .s { color: #d44950 } /* Literal.String */ 29 | .na { color: #4f9fcf } /* Name.Attribute */ 30 | .nb { color: #336666 } /* Name.Builtin */ 31 | .nc { color: #00AA88; } /* Name.Class */ 32 | .no { color: #336600 } /* Name.Constant */ 33 | .nd { color: #9999FF } /* Name.Decorator */ 34 | .ni { color: #999999; } /* Name.Entity */ 35 | .ne { color: #CC0000; } /* Name.Exception */ 36 | .nf { color: #CC00FF } /* Name.Function */ 37 | .nl { color: #9999FF } /* Name.Label */ 38 | .nn { color: #00CCFF; } /* Name.Namespace */ 39 | .nt { color: #2f6f9f; } /* Name.Tag */ 40 | .nv { color: #003333 } /* Name.Variable */ 41 | .ow { color: #000000; } /* Operator.Word */ 42 | .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .mf { color: #FF6600 } /* Literal.Number.Float */ 44 | .mh { color: #FF6600 } /* Literal.Number.Hex */ 45 | .mi { color: #FF6600 } /* Literal.Number.Integer */ 46 | .mo { color: #FF6600 } /* Literal.Number.Oct */ 47 | .sb { color: #CC3300 } /* Literal.String.Backtick */ 48 | .sc { color: #CC3300 } /* Literal.String.Char */ 49 | .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */ 50 | .s2 { color: #CC3300 } /* Literal.String.Double */ 51 | .se { color: #CC3300; } /* Literal.String.Escape */ 52 | .sh { color: #CC3300 } /* Literal.String.Heredoc */ 53 | .si { color: #AA0000 } /* Literal.String.Interpol */ 54 | .sx { color: #CC3300 } /* Literal.String.Other */ 55 | .sr { color: #33AAAA } /* Literal.String.Regex */ 56 | .s1 { color: #CC3300 } /* Literal.String.Single */ 57 | .ss { color: #FFCC33 } /* Literal.String.Symbol */ 58 | .bp { color: #336666 } /* Name.Builtin.Pseudo */ 59 | .vc { color: #003333 } /* Name.Variable.Class */ 60 | .vg { color: #003333 } /* Name.Variable.Global */ 61 | .vi { color: #003333 } /* Name.Variable.Instance */ 62 | .il { color: #FF6600 } /* Literal.Number.Integer.Long */ 63 | 64 | .css .o, 65 | .css .o + .nt, 66 | .css .nt + .nt { color: #999; } 67 | -------------------------------------------------------------------------------- /www/assets/files/west_msg.proto: -------------------------------------------------------------------------------- 1 | 2 | // WEST message specification 3 | message Message { 4 | required string event = 1; 5 | optional string channel = 2; 6 | optional string from = 3; 7 | optional string id = 4; 8 | optional bytes data = 5; 9 | } 10 | -------------------------------------------------------------------------------- /www/assets/ico/apple-touch-icon-114-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/www/assets/ico/apple-touch-icon-114-precomposed.png -------------------------------------------------------------------------------- /www/assets/ico/apple-touch-icon-144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/www/assets/ico/apple-touch-icon-144-precomposed.png -------------------------------------------------------------------------------- /www/assets/ico/apple-touch-icon-57-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/www/assets/ico/apple-touch-icon-57-precomposed.png -------------------------------------------------------------------------------- /www/assets/ico/apple-touch-icon-72-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/www/assets/ico/apple-touch-icon-72-precomposed.png -------------------------------------------------------------------------------- /www/assets/ico/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/www/assets/ico/favicon.png -------------------------------------------------------------------------------- /www/assets/js/Long.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Long.js (c) 2013 Daniel Wirtz 3 | Released under the Apache License, Version 2.0 4 | see: https://github.com/dcodeIO/Long.js for details 5 | 6 | Long.js is based on goog.math.Long from the Closure Library. 7 | Copyright 2009 The Closure Library Authors. All Rights Reserved. 8 | Released under the Apache License, Version 2.0 9 | see: https://code.google.com/p/closure-library/ for details 10 | */ 11 | var p=!1; 12 | (function(r){function b(a,b,d){this.low=a|0;this.high=b|0;this.unsigned=!!d}var s={},t={};b.fromInt=function(a,c){var d;if(c){a>>>=0;if(0<=a&&256>a&&(d=t[a]))return d;d=new b(a,0>(a|0)?-1:0,!0);0<=a&&256>a&&(t[a]=d)}else{a|=0;if(-128<=a&&128>a&&(d=s[a]))return d;d=new b(a,0>a?-1:0,p);-128<=a&&128>a&&(s[a]=d)}return d};b.fromNumber=function(a,c){c=!!c;return isNaN(a)||!isFinite(a)?b.ZERO:!c&&a<=-u?b.MIN_SIGNED_VALUE:c&&0>=a?b.MIN_UNSIGNED_VALUE:!c&&a+1>=u?b.MAX_SIGNED_VALUE:c&&a>=v?b.MAX_UNSIGNED_VALUE:0> 13 | a?b.fromNumber(-a,p).negate():new b(a%l|0,a/l|0,c)};b.fromBits=function(a,c,d){return new b(a,c,d)};b.from28Bits=function(a,c,d,e){return b.fromBits(a|c<<28,c>>>4|d<<24,e)};b.fromString=function(a,c,d){if(0==a.length)throw Error("number format error: empty string");if("NaN"===a||"Infinity"===a||"+Infinity"===a||"-Infinity"===a)return b.ZERO;"number"===typeof c&&(d=c,c=p);d=d||10;if(2>d||36f?(f=b.fromNumber(Math.pow(d,f)),e=e.multiply(f).add(b.fromNumber(k))):(e=e.multiply(c),e=e.add(b.fromNumber(k)))}return e};var l=4294967296,v=l*l,u=v/2,w=b.fromInt(16777216);b.ZERO=b.fromInt(0);b.ONE=b.fromInt(1);b.NEG_ONE=b.fromInt(-1);b.MAX_SIGNED_VALUE=b.fromBits(-1,2147483647,p); 15 | b.MAX_UNSIGNED_VALUE=b.fromBits(-1,-1,!0);b.MAX_VALUE=b.MAX_SIGNED_VALUE;b.MIN_SIGNED_VALUE=b.fromBits(0,-2147483648,p);b.MIN_UNSIGNED_VALUE=b.fromBits(0,0,!0);b.MIN_VALUE=b.MIN_SIGNED_VALUE;b.prototype.toInt=function(){return this.unsigned?this.low>>>0:this.low};b.prototype.toNumber=function(){return this.unsigned?(this.high>>>0)*l+(this.low>>>0):this.high*l+(this.low>>>0)};b.prototype.toString=function(a){a=a||10;if(2>a||36f.length;)f="0"+f;e=""+f+e}};b.prototype.getHighBits=function(){return this.high};b.prototype.getHighBitsUnsigned=function(){return this.high>>>0}; 17 | b.prototype.getLowBits=function(){return this.low};b.prototype.getLowBitsUnsigned=function(){return this.low>>>0};b.prototype.getNumBitsAbs=function(){if(this.isNegative())return this.equals(b.MIN_SIGNED_VALUE)?64:this.negate().getNumBitsAbs();for(var a=0!=this.high?this.high:this.low,c=31;0this.high};b.prototype.isOdd=function(){return 1== 18 | (this.low&1)};b.prototype.equals=function(a){return this.unsigned!=a.unsigned&&this.high>>>31!=a.high>>>31?p:this.high==a.high&&this.low==a.low};b.prototype.notEquals=function(a){return!this.equals(a)};b.prototype.lessThan=function(a){return 0>this.compare(a)};b.prototype.lessThanOrEqual=function(a){return 0>=this.compare(a)};b.prototype.greaterThan=function(a){return 0>>0>this.high>>>0||a.high==this.high&&a.low>>>0>this.low>>>0?-1:1:this.subtract(a).isNegative()?-1:1};b.prototype.negate=function(){return!this.unsigned&&this.equals(b.MIN_SIGNED_VALUE)?b.MIN_SIGNED_VALUE:this.not().add(b.ONE)};b.prototype.add=function(a){var c=this.high>>>16,d=this.high&65535,e=this.low>>>16,g=a.high>>>16,f=a.high&65535,k=a.low>>>16,q;q=0+((this.low&65535)+(a.low&65535));a=0+(q>>>16);a+=e+k;e=0+ 20 | (a>>>16);e+=d+f;d=0+(e>>>16);d=d+(c+g)&65535;return b.fromBits((a&65535)<<16|q&65535,d<<16|e&65535,this.unsigned)};b.prototype.subtract=function(a){return this.add(a.negate())};b.prototype.multiply=function(a){if(this.isZero()||a.isZero())return b.ZERO;if(this.equals(b.MIN_VALUE))return a.isOdd()?b.MIN_VALUE:b.ZERO;if(a.equals(b.MIN_VALUE))return this.isOdd()?b.MIN_VALUE:b.ZERO;if(this.isNegative())return a.isNegative()?this.negate().multiply(a.negate()):this.negate().multiply(a).negate();if(a.isNegative())return this.multiply(a.negate()).negate(); 21 | if(this.lessThan(w)&&a.lessThan(w))return b.fromNumber(this.toNumber()*a.toNumber(),this.unsigned);var c=this.high>>>16,d=this.high&65535,e=this.low>>>16,g=this.low&65535,f=a.high>>>16,k=a.high&65535,q=a.low>>>16;a=a.low&65535;var n,h,m,l;l=0+g*a;m=0+(l>>>16);m+=e*a;h=0+(m>>>16);m=(m&65535)+g*q;h+=m>>>16;m&=65535;h+=d*a;n=0+(h>>>16);h=(h&65535)+e*q;n+=h>>>16;h&=65535;h+=g*k;n+=h>>>16;h&=65535;n=n+(c*a+d*q+e*k+g*f)&65535;return b.fromBits(m<<16|l&65535,n<<16|h,this.unsigned)};b.prototype.div=function(a){if(a.isZero())throw Error("division by zero"); 22 | if(this.isZero())return b.ZERO;if(this.equals(b.MIN_SIGNED_VALUE)){if(a.equals(b.ONE)||a.equals(b.NEG_ONE))return min;if(a.equals(b.MIN_VALUE))return b.ONE;var c=this.shiftRight(1).div(a).shiftLeft(1);if(c.equals(b.ZERO))return a.isNegative()?b.ONE:b.NEG_ONE;var d=this.subtract(a.multiply(c));return c.add(d.div(a))}if(a.equals(b.MIN_VALUE))return b.ZERO;if(this.isNegative())return a.isNegative()?this.negate().div(a.negate()):this.negate().div(a).negate();if(a.isNegative())return this.div(a.negate()).negate(); 23 | for(var e=b.ZERO,d=this;d.greaterThanOrEqual(a);){for(var c=Math.max(1,Math.floor(d.toNumber()/a.toNumber())),g=Math.ceil(Math.log(c)/Math.LN2),g=48>=g?1:Math.pow(2,g-48),f=b.fromNumber(c,this.unsigned),k=f.multiply(a);k.isNegative()||k.greaterThan(d);)c-=g,f=b.fromNumber(c,this.unsigned),k=f.multiply(a);f.isZero()&&(f=b.ONE);e=e.add(f);d=d.subtract(k)}return e};b.prototype.modulo=function(a){return this.subtract(this.div(a).multiply(a))};b.prototype.not=function(){return b.fromBits(~this.low,~this.high, 24 | this.unsigned)};b.prototype.and=function(a){return b.fromBits(this.low&a.low,this.high&a.high,this.unsigned)};b.prototype.or=function(a){return b.fromBits(this.low|a.low,this.high|a.high,this.unsigned)};b.prototype.xor=function(a){return b.fromBits(this.low^a.low,this.high^a.high,this.unsigned)};b.prototype.shiftLeft=function(a){a&=63;if(0==a)return this;var c=this.low;return 32>a?b.fromBits(c<>>32-a,this.unsigned):b.fromBits(0,c<a?b.fromBits(this.low>>>a|c<<32-a,c>>a,this.unsigned):b.fromBits(c>>a-32,0<=c?0:-1,this.unsigned)};b.prototype.shiftRightUnsigned=function(a){a&=63;if(0==a)return this;var c=this.high;return 32>a?b.fromBits(this.low>>>a|c<<32-a,c>>>a,this.unsigned):32==a?b.fromBits(c,0,this.unsigned):b.fromBits(c>>>a-32,0,this.unsigned)};b.prototype.toSigned=function(){var a=this.clone();a.unsigned=p;return a};b.prototype.toUnsigned=function(){var a=this.clone();a.unsigned= 26 | !0;return a};b.prototype.clone=function(){return new b(this.low,this.high,this.unsigned)};"undefined"!=typeof module&&module.exports?module.exports=b:"undefined"!=typeof define&&define.amd?define("Math/Long",[],function(){return b}):(r.dcodeIO||(r.dcodeIO={}),r.dcodeIO.Long=b)})(this); 27 | -------------------------------------------------------------------------------- /www/assets/js/application.js: -------------------------------------------------------------------------------- 1 | // NOTICE!! DO NOT USE ANY OF THIS JAVASCRIPT 2 | // IT'S ALL JUST JUNK FOR OUR DOCS! 3 | // ++++++++++++++++++++++++++++++++++++++++++ 4 | 5 | !function ($) { 6 | 7 | $(function(){ 8 | 9 | var $window = $(window) 10 | var $body = $(document.body) 11 | 12 | var navHeight = $('.navbar').outerHeight(true) + 10 13 | 14 | $body.scrollspy({ 15 | target: '.bs-sidebar', 16 | offset: navHeight 17 | }) 18 | 19 | $window.on('load', function () { 20 | $body.scrollspy('refresh') 21 | }) 22 | 23 | $('.bs-docs-container [href=#]').click(function (e) { 24 | e.preventDefault() 25 | }) 26 | 27 | // back to top 28 | setTimeout(function () { 29 | var $sideBar = $('.bs-sidebar') 30 | 31 | $sideBar.affix({ 32 | offset: { 33 | top: function () { 34 | var offsetTop = $sideBar.offset().top 35 | var sideBarMargin = parseInt($sideBar.children(0).css('margin-top'), 10) 36 | var navOuterHeight = $('.bs-docs-nav').height() 37 | 38 | return (this.top = offsetTop - navOuterHeight - sideBarMargin) 39 | } 40 | , bottom: function () { 41 | return (this.bottom = $('.bs-footer').outerHeight(true)) 42 | } 43 | } 44 | }) 45 | }, 100) 46 | 47 | setTimeout(function () { 48 | $('.bs-top').affix() 49 | }, 100) 50 | 51 | // tooltip demo 52 | $('.tooltip-demo').tooltip({ 53 | selector: "[data-toggle=tooltip]", 54 | container: "body" 55 | }) 56 | 57 | $('.tooltip-test').tooltip() 58 | $('.popover-test').popover() 59 | 60 | $('.bs-docs-navbar').tooltip({ 61 | selector: "a[data-toggle=tooltip]", 62 | container: ".bs-docs-navbar .nav" 63 | }) 64 | 65 | // popover demo 66 | $("[data-toggle=popover]") 67 | .popover() 68 | 69 | // button state demo 70 | $('#fat-btn') 71 | .click(function () { 72 | var btn = $(this) 73 | btn.button('loading') 74 | setTimeout(function () { 75 | btn.button('reset') 76 | }, 3000) 77 | }) 78 | 79 | // carousel demo 80 | $('.bs-docs-carousel-example').carousel() 81 | }) 82 | 83 | }(window.jQuery) 84 | -------------------------------------------------------------------------------- /www/assets/js/filesaver.js: -------------------------------------------------------------------------------- 1 | /* Blob.js 2 | * A Blob implementation. 3 | * 2013-06-20 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * By Devin Samarin, https://github.com/eboyjr 7 | * License: X11/MIT 8 | * See LICENSE.md 9 | */ 10 | 11 | /*global self, unescape */ 12 | /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, 13 | plusplus: true */ 14 | 15 | /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ 16 | 17 | if (typeof Blob !== "function" || typeof URL === "undefined") 18 | if (typeof Blob === "function" && typeof webkitURL !== "undefined") self.URL = webkitURL; 19 | else var Blob = (function (view) { 20 | "use strict"; 21 | 22 | var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || view.MSBlobBuilder || (function(view) { 23 | var 24 | get_class = function(object) { 25 | return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; 26 | } 27 | , FakeBlobBuilder = function BlobBuilder() { 28 | this.data = []; 29 | } 30 | , FakeBlob = function Blob(data, type, encoding) { 31 | this.data = data; 32 | this.size = data.length; 33 | this.type = type; 34 | this.encoding = encoding; 35 | } 36 | , FBB_proto = FakeBlobBuilder.prototype 37 | , FB_proto = FakeBlob.prototype 38 | , FileReaderSync = view.FileReaderSync 39 | , FileException = function(type) { 40 | this.code = this[this.name = type]; 41 | } 42 | , file_ex_codes = ( 43 | "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " 44 | + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" 45 | ).split(" ") 46 | , file_ex_code = file_ex_codes.length 47 | , real_URL = view.URL || view.webkitURL || view 48 | , real_create_object_URL = real_URL.createObjectURL 49 | , real_revoke_object_URL = real_URL.revokeObjectURL 50 | , URL = real_URL 51 | , btoa = view.btoa 52 | , atob = view.atob 53 | 54 | , ArrayBuffer = view.ArrayBuffer 55 | , Uint8Array = view.Uint8Array 56 | ; 57 | FakeBlob.fake = FB_proto.fake = true; 58 | while (file_ex_code--) { 59 | FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; 60 | } 61 | if (!real_URL.createObjectURL) { 62 | URL = view.URL = {}; 63 | } 64 | URL.createObjectURL = function(blob) { 65 | var 66 | type = blob.type 67 | , data_URI_header 68 | ; 69 | if (type === null) { 70 | type = "application/octet-stream"; 71 | } 72 | if (blob instanceof FakeBlob) { 73 | data_URI_header = "data:" + type; 74 | if (blob.encoding === "base64") { 75 | return data_URI_header + ";base64," + blob.data; 76 | } else if (blob.encoding === "URI") { 77 | return data_URI_header + "," + decodeURIComponent(blob.data); 78 | } if (btoa) { 79 | return data_URI_header + ";base64," + btoa(blob.data); 80 | } else { 81 | return data_URI_header + "," + encodeURIComponent(blob.data); 82 | } 83 | } else if (real_create_object_URL) { 84 | return real_create_object_URL.call(real_URL, blob); 85 | } 86 | }; 87 | URL.revokeObjectURL = function(object_URL) { 88 | if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { 89 | real_revoke_object_URL.call(real_URL, object_URL); 90 | } 91 | }; 92 | FBB_proto.append = function(data/*, endings*/) { 93 | var bb = this.data; 94 | // decode data to a binary string 95 | if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { 96 | var 97 | str = "" 98 | , buf = new Uint8Array(data) 99 | , i = 0 100 | , buf_len = buf.length 101 | ; 102 | for (; i < buf_len; i++) { 103 | str += String.fromCharCode(buf[i]); 104 | } 105 | bb.push(str); 106 | } else if (get_class(data) === "Blob" || get_class(data) === "File") { 107 | if (FileReaderSync) { 108 | var fr = new FileReaderSync; 109 | bb.push(fr.readAsBinaryString(data)); 110 | } else { 111 | // async FileReader won't work as BlobBuilder is sync 112 | throw new FileException("NOT_READABLE_ERR"); 113 | } 114 | } else if (data instanceof FakeBlob) { 115 | if (data.encoding === "base64" && atob) { 116 | bb.push(atob(data.data)); 117 | } else if (data.encoding === "URI") { 118 | bb.push(decodeURIComponent(data.data)); 119 | } else if (data.encoding === "raw") { 120 | bb.push(data.data); 121 | } 122 | } else { 123 | if (typeof data !== "string") { 124 | data += ""; // convert unsupported types to strings 125 | } 126 | // decode UTF-16 to binary string 127 | bb.push(unescape(encodeURIComponent(data))); 128 | } 129 | }; 130 | FBB_proto.getBlob = function(type) { 131 | if (!arguments.length) { 132 | type = null; 133 | } 134 | return new FakeBlob(this.data.join(""), type, "raw"); 135 | }; 136 | FBB_proto.toString = function() { 137 | return "[object BlobBuilder]"; 138 | }; 139 | FB_proto.slice = function(start, end, type) { 140 | var args = arguments.length; 141 | if (args < 3) { 142 | type = null; 143 | } 144 | return new FakeBlob( 145 | this.data.slice(start, args > 1 ? end : this.data.length) 146 | , type 147 | , this.encoding 148 | ); 149 | }; 150 | FB_proto.toString = function() { 151 | return "[object Blob]"; 152 | }; 153 | return FakeBlobBuilder; 154 | }(view)); 155 | 156 | return function Blob(blobParts, options) { 157 | var type = options ? (options.type || "") : ""; 158 | var builder = new BlobBuilder(); 159 | if (blobParts) { 160 | for (var i = 0, len = blobParts.length; i < len; i++) { 161 | builder.append(blobParts[i]); 162 | } 163 | } 164 | return builder.getBlob(type); 165 | }; 166 | }(self)); 167 | 168 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 169 | var saveAs=saveAs||(navigator.msSaveOrOpenBlob&&navigator.msSaveOrOpenBlob.bind(navigator))||(function(h){"use strict";var r=h.document,l=function(){return h.URL||h.webkitURL||h},e=h.URL||h.webkitURL||h,n=r.createElementNS("http://www.w3.org/1999/xhtml","a"),g=!h.externalHost&&"download" in n,j=function(t){var s=r.createEvent("MouseEvents");s.initMouseEvent("click",true,false,h,0,0,0,0,0,false,false,false,false,0,null);t.dispatchEvent(s)},o=h.webkitRequestFileSystem,p=h.requestFileSystem||o||h.mozRequestFileSystem,m=function(s){(h.setImmediate||h.setTimeout)(function(){throw s},0)},c="application/octet-stream",k=0,b=[],i=function(){var t=b.length;while(t--){var s=b[t];if(typeof s==="string"){e.revokeObjectURL(s)}else{s.remove()}}b.length=0},q=function(t,s,w){s=[].concat(s);var v=s.length;while(v--){var x=t["on"+s[v]];if(typeof x==="function"){try{x.call(t,w||t)}catch(u){m(u)}}}},f=function(t,u){var v=this,B=t.type,E=false,x,w,s=function(){var F=l().createObjectURL(t);b.push(F);return F},A=function(){q(v,"writestart progress write writeend".split(" "))},D=function(){if(E||!x){x=s(t)}if(w){w.location.href=x}else{window.open(x,"_blank")}v.readyState=v.DONE;A()},z=function(F){return function(){if(v.readyState!==v.DONE){return F.apply(this,arguments)}}},y={create:true,exclusive:false},C;v.readyState=v.INIT;if(!u){u="download"}if(g){x=s(t);n.href=x;n.download=u;j(n);v.readyState=v.DONE;A();return}if(h.chrome&&B&&B!==c){C=t.slice||t.webkitSlice;t=C.call(t,0,t.size,c);E=true}if(o&&u!=="download"){u+=".download"}if(B===c||o){w=h}if(!p){D();return}k+=t.size;p(h.TEMPORARY,k,z(function(F){F.root.getDirectory("saved",y,z(function(G){var H=function(){G.getFile(u,y,z(function(I){I.createWriter(z(function(J){J.onwriteend=function(K){w.location.href=I.toURL();b.push(I);v.readyState=v.DONE;q(v,"writeend",K)};J.onerror=function(){var K=J.error;if(K.code!==K.ABORT_ERR){D()}};"writestart progress write abort".split(" ").forEach(function(K){J["on"+K]=v["on"+K]});J.write(t);v.abort=function(){J.abort();v.readyState=v.DONE};v.readyState=v.WRITING}),D)}),D)};G.getFile(u,{create:false},z(function(I){I.remove();H()}),z(function(I){if(I.code===I.NOT_FOUND_ERR){H()}else{D()}}))}),D)}),D)},d=f.prototype,a=function(s,t){return new f(s,t)};d.abort=function(){var s=this;s.readyState=s.DONE;q(s,"abort")};d.readyState=d.INIT=0;d.WRITING=1;d.DONE=2;d.error=d.onwritestart=d.onprogress=d.onwrite=d.onabort=d.onerror=d.onwriteend=null;h.addEventListener("unload",i,false);return a}(self)); -------------------------------------------------------------------------------- /www/assets/js/html5shiv.js: -------------------------------------------------------------------------------- 1 | /* 2 | HTML5 Shiv v3.6.2pre | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | (function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); 5 | a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/\w+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x"; 6 | c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode|| 7 | "undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",version:"3.6.2pre",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);if(g)return a.createDocumentFragment(); 8 | for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d #mq-test-1 { width: 42px; }',d.insertBefore(f,e),c=42===g.offsetWidth,d.removeChild(f),{matches:c,media:a}}}(document); 4 | 5 | /*! Respond.js v1.1.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */ 6 | (function(a){"use strict";function x(){u(!0)}var b={};a.respond=b,b.update=function(){},b.mediaQueriesSupported=a.matchMedia&&a.matchMedia("only all").matches,b.mediaQueriesSupported;var q,r,t,c=a.document,d=c.documentElement,e=[],f=[],g=[],h={},i=30,j=c.getElementsByTagName("head")[0]||d,k=c.getElementsByTagName("base")[0],l=j.getElementsByTagName("link"),m=[],n=function(){for(var b=0;l.length>b;b++){var c=l[b],d=c.href,e=c.media,f=c.rel&&"stylesheet"===c.rel.toLowerCase();d&&f&&!h[d]&&(c.styleSheet&&c.styleSheet.rawCssText?(p(c.styleSheet.rawCssText,d,e),h[d]=!0):(!/^([a-zA-Z:]*\/\/)/.test(d)&&!k||d.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&m.push({href:d,media:e}))}o()},o=function(){if(m.length){var a=m.shift();v(a.href,function(b){p(b,a.href,a.media),h[a.href]=!0,setTimeout(function(){o()},0)})}},p=function(a,b,c){var d=a.match(/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi),g=d&&d.length||0;b=b.substring(0,b.lastIndexOf("/"));var h=function(a){return a.replace(/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,"$1"+b+"$2$3")},i=!g&&c;b.length&&(b+="/"),i&&(g=1);for(var j=0;g>j;j++){var k,l,m,n;i?(k=c,f.push(h(a))):(k=d[j].match(/@media *([^\{]+)\{([\S\s]+?)$/)&&RegExp.$1,f.push(RegExp.$2&&h(RegExp.$2))),m=k.split(","),n=m.length;for(var o=0;n>o;o++)l=m[o],e.push({media:l.split("(")[0].match(/(only\s+)?([a-zA-Z]+)\s?/)&&RegExp.$2||"all",rules:f.length-1,hasquery:l.indexOf("(")>-1,minw:l.match(/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:l.match(/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},s=function(){var a,b=c.createElement("div"),e=c.body,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",e||(e=f=c.createElement("body"),e.style.background="none"),e.appendChild(b),d.insertBefore(e,d.firstChild),a=b.offsetWidth,f?d.removeChild(e):e.removeChild(b),a=t=parseFloat(a)},u=function(a){var b="clientWidth",h=d[b],k="CSS1Compat"===c.compatMode&&h||c.body[b]||h,m={},n=l[l.length-1],o=(new Date).getTime();if(a&&q&&i>o-q)return clearTimeout(r),r=setTimeout(u,i),void 0;q=o;for(var p in e)if(e.hasOwnProperty(p)){var v=e[p],w=v.minw,x=v.maxw,y=null===w,z=null===x,A="em";w&&(w=parseFloat(w)*(w.indexOf(A)>-1?t||s():1)),x&&(x=parseFloat(x)*(x.indexOf(A)>-1?t||s():1)),v.hasquery&&(y&&z||!(y||k>=w)||!(z||x>=k))||(m[v.media]||(m[v.media]=[]),m[v.media].push(f[v.rules]))}for(var B in g)g.hasOwnProperty(B)&&g[B]&&g[B].parentNode===j&&j.removeChild(g[B]);for(var C in m)if(m.hasOwnProperty(C)){var D=c.createElement("style"),E=m[C].join("\n");D.type="text/css",D.media=C,j.insertBefore(D,n.nextSibling),D.styleSheet?D.styleSheet.cssText=E:D.appendChild(c.createTextNode(E)),g.push(D)}},v=function(a,b){var c=w();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))},w=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}();n(),b.update=n,a.addEventListener?a.addEventListener("resize",x,!1):a.attachEvent&&a.attachEvent("onresize",x)})(this); 7 | -------------------------------------------------------------------------------- /www/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/www/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /www/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/www/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /www/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cabol/west/c3c31dff9ad727ce9b82dde6eb690f7b11cd4d24/www/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /www/css/my_theme.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100%; 3 | } 4 | 5 | /* Wrapper for page content to push down footer */ 6 | #wrap { 7 | min-height: 100%; 8 | height: auto !important; 9 | height: 100%; 10 | /* Negative indent footer by its height */ 11 | margin: 0 auto -60px; 12 | /* Pad bottom by footer height */ 13 | padding: 0 0 60px; 14 | } 15 | #wrap > .container { 16 | padding: 30px 15px 0; 17 | } 18 | 19 | /* Set the fixed height of the footer here */ 20 | #footer { 21 | height: 60px; 22 | background-color: #f5f5f5; 23 | } 24 | #footer > .container { 25 | padding-top: 20px; 26 | padding-left: 15px; 27 | padding-right: 15px; 28 | } 29 | 30 | /* Jumbotron */ 31 | #wrap > .container .jumbotron { 32 | text-align: center; 33 | background-color: transparent; 34 | } 35 | #wrap > .container .jumbotron .btn { 36 | padding: 14px 24px; 37 | font-size: 21px; 38 | } 39 | 40 | #wrap > .container .row { 41 | padding-bottom: 40px; 42 | } 43 | -------------------------------------------------------------------------------- /www/css/starter-template.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | padding-top: 20px; 3 | padding-bottom: 20px; 4 | height: 100%; 5 | } 6 | 7 | .form-inline .form-group { 8 | text-align: left; 9 | display: inline-block; 10 | } 11 | 12 | /* Set the fixed height of the footer here */ 13 | #footer { 14 | position: relative; 15 | height: 60px; 16 | background-color: #f5f5f5; 17 | margin-top: 100px; 18 | } 19 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | WEST 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 |
    29 | 30 |
    31 | 32 | 49 | 50 | 51 |
    52 |

    Web/Event-driven Systems Tool

    53 |

    A new way to build real-time and high scalable messaging-based applications, not centralized but distributed!

    54 |

    Source Code

    55 |
    56 | 57 | 58 |
    59 |
    60 |

    JSON Wire Protocol

    61 |

    JSON is widely used in web-based applications as application level protocol, 62 | also used to define public APIs. Is supported by all programming languages. 63 | Recommended strongly for browser-based clients.

    64 |

    Get started »

    65 |
    66 |
    67 |

    Protocol Buffers

    68 |

    Highly recommended for performance-critical scenarios. PB produces quite 69 | compact data (small output), and provides very fast processing reducing 70 | significantly the CPU overhead.

    71 |

    Get started »

    72 |
    73 |
    74 |

    Text Wire Protocol

    75 |

    Just for testing purposes.

    76 |

    Get started »

    77 |
    78 |
    79 | 80 |
    81 | 82 |
    83 | 84 | 89 | 90 | 91 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /www/int/jsonwp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | JSON Wire Protocol 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 78 | 79 | 80 | 81 | 82 | 83 |
    84 | 85 |
    86 | 87 | 104 | 105 | 108 | 109 |
      110 |
    • Ping to WEST: {"event":"ping"}
    • 111 |
    • Register to a Channel: {"event":"register", "channel":"channel_name"}
    • 112 |
    • Unregister from a Channel: {"event":"unregister", "channel":"channel_name"}
    • 113 |
    • Send to a Channel: {"event":"send", "channel":"channel_name", "data":"..your message.."}
    • 114 |
    • Subscribe to a Channel: {"event":"subscribe", "channel":"channel_name"}
    • 115 |
    • Unsubscribe from a Channel: {"event":"unsubscribe", "channel":"channel_name"}
    • 116 |
    • Publish to a Channel: {"event":"publish", "channel":"channel_name", "data":"..your message.."}
    • 117 |
    118 | 119 |
    120 | 123 |
    124 | 127 |
    128 |
    129 | 130 |
    131 |
    132 | 133 |
    134 | 135 |
    136 |
    137 | 138 |
    139 | 140 | 141 |
    142 | 143 |
    144 | 145 | 172 | 173 |
    174 | 175 |
    176 | 177 | 182 | 183 | 187 | 188 | 190 | 191 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /www/int/textwp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Text Wire Protocol 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 76 | 77 | 78 | 79 | 80 | 81 |
    82 | 83 |
    84 | 85 | 102 | 103 | 106 | 107 |
      108 |
    • Close connection: bye
    • 109 |
    • Ping to WEST: ping
    • 110 |
    • Register to a channel: reg channel_name
    • 111 |
    • Unregister from a channel: unreg channel_name
    • 112 |
    • Send message to a channel: send channel_name "... your message ..."
    • 113 |
    • Subscribe to a channel: sub channel_name
    • 114 |
    • Publish message to a channel: pub channel_name "... your message ..."
    • 115 |
    • Unsubscribe from a channel: unsub channel_name
    • 116 |
    • Other messages are echoed
    • 117 |
    118 | 119 |
    120 | 123 |
    124 | 127 |
    128 |
    129 | 130 |
    131 |
    132 | 133 |
    134 | 135 |
    136 |
    137 | 138 |
    139 | 140 | 141 |
    142 | 143 |
    144 | 145 | 171 | 172 |
    173 | 174 |
    175 | 176 | 181 | 182 | 186 | 187 | 189 | 190 | 192 | 193 | 194 | --------------------------------------------------------------------------------