fuzed 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/LICENSE +23 -0
- data/Manifest.txt +32 -0
- data/README.txt +81 -0
- data/Rakefile +36 -0
- data/bin/fuzed +49 -0
- data/bin/fuzed-adapter +291 -0
- data/bin/fuzed-conf +23 -0
- data/elibs/port_wrapper.beam +0 -0
- data/elibs/port_wrapper.erl +55 -0
- data/elibs/rails_connection_pool.beam +0 -0
- data/elibs/rails_connection_pool.erl +147 -0
- data/elibs/rails_forwarder.beam +0 -0
- data/elibs/rails_forwarder.erl +112 -0
- data/elibs/resource_manager.beam +0 -0
- data/elibs/resource_manager.erl +108 -0
- data/elibs/test_rails_handler.beam +0 -0
- data/elibs/test_rails_handler.erl +11 -0
- data/join_cluster.beam +0 -0
- data/join_cluster.erl +26 -0
- data/lib/fuzed.rb +13 -0
- data/rails_responder +3 -0
- data/sample.fuzed.conf +38 -0
- data/start_node +6 -0
- data/start_yaws +5 -0
- data/templates/fuzed.conf +38 -0
- data/testing.conf +177 -0
- data/yaws_includes/erlsom.hrl +28 -0
- data/yaws_includes/soap.hrl +48 -0
- data/yaws_includes/yaws.hrl +271 -0
- data/yaws_includes/yaws_api.hrl +94 -0
- data/yaws_includes/yaws_dav.hrl +11 -0
- metadata +101 -0
data/bin/fuzed-conf
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
puts("You must specify the absolute path to your Rails root as the first argument") and exit unless ARGV[0]
|
4
|
+
|
5
|
+
rails_public_dir = File.join(ARGV[0], 'public')
|
6
|
+
|
7
|
+
SUBSTITUTIONS =
|
8
|
+
{:rails_public_dir => rails_public_dir,
|
9
|
+
:port => ARGV[1] || "80"}
|
10
|
+
|
11
|
+
template = File.join(File.dirname(__FILE__), *%w[.. templates fuzed.conf])
|
12
|
+
|
13
|
+
conf = File.open(template).read
|
14
|
+
SUBSTITUTIONS.each_pair do |key, data|
|
15
|
+
repl_key = '{{ ' + key.to_s.upcase + ' }}'
|
16
|
+
conf.gsub!(repl_key, data)
|
17
|
+
end
|
18
|
+
|
19
|
+
File.open('fuzed.conf', 'w') do |f|
|
20
|
+
f.write conf
|
21
|
+
end
|
22
|
+
|
23
|
+
puts 'successfully wrote fuzed.conf in current directory'
|
Binary file
|
@@ -0,0 +1,55 @@
|
|
1
|
+
-module(port_wrapper).
|
2
|
+
-export([wrap/1, send/2, send_from/3, shutdown/1, rpc/2]).
|
3
|
+
-author('Dave Fayram').
|
4
|
+
|
5
|
+
wrap(Command) ->
|
6
|
+
spawn(fun() -> process_flag(trap_exit, true), Port = create_port(Command), link(Port), loop(Port, [
|
7
|
+
]) end).
|
8
|
+
|
9
|
+
rpc(WrappedPort, Message) ->
|
10
|
+
send(WrappedPort, Message),
|
11
|
+
receive
|
12
|
+
{WrappedPort, Result} -> Result
|
13
|
+
after 10000 ->
|
14
|
+
{WrappedPort, timed_out}
|
15
|
+
end.
|
16
|
+
|
17
|
+
send(WrappedPort, Message) ->
|
18
|
+
WrappedPort ! {self(), {command, term_to_binary(Message)}},
|
19
|
+
WrappedPort.
|
20
|
+
|
21
|
+
send_from(Target, WrappedPort, Message) ->
|
22
|
+
WrappedPort ! {Target, {command, term_to_binary(Message)}},
|
23
|
+
WrappedPort.
|
24
|
+
|
25
|
+
|
26
|
+
shutdown(WrappedPort) ->
|
27
|
+
WrappedPort ! shutdown,
|
28
|
+
true.
|
29
|
+
|
30
|
+
create_port(Command) ->
|
31
|
+
open_port({spawn, Command}, [{packet, 4}, nouse_stdio, exit_status, binary]).
|
32
|
+
|
33
|
+
loop(Port, Monitors) ->
|
34
|
+
receive
|
35
|
+
shutdown ->
|
36
|
+
port_close(Port),
|
37
|
+
exit(shutdown);
|
38
|
+
{Source, {command, Message}} ->
|
39
|
+
Port ! {self(), {command, Message}},
|
40
|
+
receive
|
41
|
+
{Port, {data, Result}} ->
|
42
|
+
Source ! {self(), binary_to_term(Result)}
|
43
|
+
end,
|
44
|
+
loop(Port,Monitors);
|
45
|
+
|
46
|
+
{monitor, Proc} when is_pid(Proc) ->
|
47
|
+
loop(Port, [Proc|Monitors]);
|
48
|
+
|
49
|
+
{Port, {exit_status, _Code}} ->
|
50
|
+
%port_close(Port)
|
51
|
+
exit(external_failure);
|
52
|
+
Any ->
|
53
|
+
io:format("----------~nWrapper got: ~p~n----------~n", [Any]),
|
54
|
+
loop(Port, Monitors)
|
55
|
+
end.
|
Binary file
|
@@ -0,0 +1,147 @@
|
|
1
|
+
-module(rails_connection_pool).
|
2
|
+
-compile(export_all).
|
3
|
+
|
4
|
+
|
5
|
+
%% Convenience Function
|
6
|
+
simple_handle_request(Arg,ServerInfo) ->
|
7
|
+
{Source, Resource} = rails_connection_pool:get(),
|
8
|
+
Response = rails_forwarder:handle_request(Resource, Arg, ServerInfo, 10000),
|
9
|
+
rails_connection_pool:refund({Source,Resource}),
|
10
|
+
Response.
|
11
|
+
|
12
|
+
|
13
|
+
handle_request(Arg,ServerInfo) ->
|
14
|
+
handle_request_helper(Arg,ServerInfo,0).
|
15
|
+
|
16
|
+
handle_request_helper(_Arg,_ServerInfo,Retries) when Retries > 1 ->
|
17
|
+
throw(timed_out);
|
18
|
+
handle_request_helper(Arg,ServerInfo,Retries) ->
|
19
|
+
Remote = fun(A,S,Target) ->
|
20
|
+
{Source, Handler} = rails_connection_pool:get(),
|
21
|
+
Response = rails_forwarder:rails_handle_request(Handler, A, S, 10000),
|
22
|
+
Target ! {rails_response, Response},
|
23
|
+
rails_connection_pool:refund({Source, Handler}) end,
|
24
|
+
_RequestProc = spawn(Remote(Arg,ServerInfo,self())),
|
25
|
+
receive
|
26
|
+
{rails_response, Response} ->
|
27
|
+
Response
|
28
|
+
after 5000 ->
|
29
|
+
handle_request_helper(Arg,ServerInfo, Retries+1)
|
30
|
+
end.
|
31
|
+
|
32
|
+
|
33
|
+
%% Server manipulation
|
34
|
+
start() ->
|
35
|
+
global:register_name(?MODULE, spawn(
|
36
|
+
fun() ->
|
37
|
+
process_flag(trap_exit, true),
|
38
|
+
rails_connection_pool:loop([],[]) end
|
39
|
+
)
|
40
|
+
).
|
41
|
+
|
42
|
+
add({Node, Proc}) when is_pid(Proc) ->
|
43
|
+
global:send(?MODULE, {add, {Node, Proc}}),
|
44
|
+
ok.
|
45
|
+
|
46
|
+
remove(Rsrc) ->
|
47
|
+
global:send(?MODULE, {remove, Rsrc}),
|
48
|
+
ok.
|
49
|
+
|
50
|
+
get() ->
|
51
|
+
global:send(?MODULE, {get, self()}),
|
52
|
+
receive
|
53
|
+
{node, X} ->
|
54
|
+
X
|
55
|
+
end.
|
56
|
+
|
57
|
+
refund(Node) ->
|
58
|
+
global:send(?MODULE, {refund, Node}),
|
59
|
+
ok.
|
60
|
+
|
61
|
+
list() ->
|
62
|
+
global:send(?MODULE, {list, self()}),
|
63
|
+
receive
|
64
|
+
{nodes, A} ->
|
65
|
+
A
|
66
|
+
end.
|
67
|
+
|
68
|
+
list_all() ->
|
69
|
+
global:send(?MODULE, {list_all, self()}),
|
70
|
+
receive
|
71
|
+
{all_nodes, A} ->
|
72
|
+
A
|
73
|
+
end.
|
74
|
+
|
75
|
+
remove_server(Server) ->
|
76
|
+
global:send(?MODULE, {remove_server, Server}),
|
77
|
+
ok.
|
78
|
+
|
79
|
+
remove_all() ->
|
80
|
+
global:send(?MODULE, {remove_all}),
|
81
|
+
ok.
|
82
|
+
|
83
|
+
remove_server_filter(Server, {Server, _X}) -> false;
|
84
|
+
remove_server_filter(_Server, {_NotServer, _X}) -> true.
|
85
|
+
|
86
|
+
loop([],A) ->
|
87
|
+
receive
|
88
|
+
{add, {Node, Proc}} when is_pid(Proc) ->
|
89
|
+
erlang:link(Proc),
|
90
|
+
loop([{Node,Proc}],[{Node,Proc}|A]);
|
91
|
+
{list, Pid} ->
|
92
|
+
Pid ! {nodes, []},
|
93
|
+
loop([],A);
|
94
|
+
{list_all, Pid} ->
|
95
|
+
Pid ! {all_nodes, A},
|
96
|
+
loop([],A);
|
97
|
+
{refund,Node} ->
|
98
|
+
Membership = lists:member(Node,A),
|
99
|
+
if
|
100
|
+
Membership ->
|
101
|
+
loop([Node],A);
|
102
|
+
true ->
|
103
|
+
loop([],A)
|
104
|
+
end;
|
105
|
+
{'EXIT', Pid, _Reason} ->
|
106
|
+
FilterFun = fun({_Node,MPid}) -> MPid /= Pid end,
|
107
|
+
loop([],lists:filter(FilterFun, A))
|
108
|
+
end;
|
109
|
+
loop(X,A) ->
|
110
|
+
receive
|
111
|
+
{add, {Node, Proc}} when is_pid(Proc)->
|
112
|
+
erlang:link(Proc),
|
113
|
+
I = {Node, Proc},
|
114
|
+
loop([I|X], [I|A]);
|
115
|
+
{remove, I} ->
|
116
|
+
loop(lists:delete(I,X), lists:delete(I,A));
|
117
|
+
{remove_server, Server} ->
|
118
|
+
PurgedX = lists:filter(fun(Z) -> remove_server_filter(Server, Z) end, X),
|
119
|
+
PurgedA = lists:filter(fun(Z) -> remove_server_filter(Server, Z) end, A),
|
120
|
+
loop(PurgedX, PurgedA);
|
121
|
+
{remove_all} ->
|
122
|
+
loop([],[]);
|
123
|
+
{list, Pid} ->
|
124
|
+
Pid ! {nodes, X},
|
125
|
+
loop(X,A);
|
126
|
+
{list_all, Pid} ->
|
127
|
+
Pid ! {all_nodes, A},
|
128
|
+
loop(X,A);
|
129
|
+
{get, Pid} ->
|
130
|
+
[Node|Rest] = X,
|
131
|
+
Pid ! {node, Node},
|
132
|
+
loop(Rest,A);
|
133
|
+
{refund,Node} ->
|
134
|
+
Membership = lists:member(Node,A),
|
135
|
+
if
|
136
|
+
Membership ->
|
137
|
+
loop(X ++ [Node],A);
|
138
|
+
true ->
|
139
|
+
loop(X,A)
|
140
|
+
end;
|
141
|
+
{'EXIT', Pid, _Reason} ->
|
142
|
+
FilterFun = fun({_Node,MPid}) -> MPid /= Pid end,
|
143
|
+
loop(lists:filter(FilterFun,X),lists:filter(FilterFun, A));
|
144
|
+
Other ->
|
145
|
+
io:format("~p loop(X,A) Received unknown message: ~p~n", [?MODULE, Other]),
|
146
|
+
loop(X,A)
|
147
|
+
end.
|
Binary file
|
@@ -0,0 +1,112 @@
|
|
1
|
+
-module(rails_forwarder).
|
2
|
+
-compile(export_all).
|
3
|
+
-include("../yaws_includes/yaws_api.hrl").
|
4
|
+
-include("../yaws_includes/yaws.hrl").
|
5
|
+
|
6
|
+
%-export([rails_create_responder/1, rails_create_responders/2, rails_handle_request/3,
|
7
|
+
% rails_handle_request_for/3, get_rails_response/2, prepare_request/1, decode_result/1]).
|
8
|
+
|
9
|
+
%create_responder(Command) ->
|
10
|
+
% port_wrapper:wrap(Command).
|
11
|
+
%
|
12
|
+
%create_responders(Command, N) ->
|
13
|
+
% create_responders_helper(Command, N, []).
|
14
|
+
%
|
15
|
+
%create_responders_helper(_Command, 0, Results) ->
|
16
|
+
% Results;
|
17
|
+
%create_responders_helper(Command, N, Results) ->
|
18
|
+
% create_responders_helper(Command, N-1, [create_responder(Command)|Results]).
|
19
|
+
|
20
|
+
handle_request(Resource, Request, ServerOptions, Timeout) ->
|
21
|
+
{_Node, Responder} = Resource,
|
22
|
+
handle_request_for(Responder, Request, ServerOptions, self()),
|
23
|
+
try get_rails_response(Responder, Timeout) of
|
24
|
+
Any -> Any
|
25
|
+
catch
|
26
|
+
throw:timed_out ->
|
27
|
+
io:format("--- Timed out, removing resource and retrying!"),
|
28
|
+
rails_connection_pool:remove(Resource),
|
29
|
+
rails_forwarder:handle_request(rails_connection_pool:get(), Request, ServerOptions, Timeout)
|
30
|
+
end.
|
31
|
+
|
32
|
+
handle_request_for(Responder, Request, ServerOptions, Target) ->
|
33
|
+
Data = {request, prepare_request(Request, ServerOptions)},
|
34
|
+
port_wrapper:send_from(Target, Responder, Data).
|
35
|
+
|
36
|
+
|
37
|
+
get_rails_response(Responder, Timeout) ->
|
38
|
+
receive
|
39
|
+
{Responder, Result} ->
|
40
|
+
decode_result(Result);
|
41
|
+
Any ->
|
42
|
+
io:format("Unexpected message in get_rails_response: ~p~n", [Any])
|
43
|
+
after Timeout ->
|
44
|
+
throw(timed_out)
|
45
|
+
end.
|
46
|
+
|
47
|
+
|
48
|
+
% Fugly data munging
|
49
|
+
prepare_request(Request, ServerOptions) ->
|
50
|
+
Headers = Request#arg.headers,
|
51
|
+
{convert_method(Request), convert_version(Request), convert_querypath(Request),
|
52
|
+
{querydata, prep(Request#arg.querydata)}, {servername, prep(ServerOptions#sconf.servername)},
|
53
|
+
{headers, convert_headers(Request#arg.headers)},
|
54
|
+
{cookies, list_to_tuple(lists:map(fun(X) -> prep(X) end, Headers#headers.cookie))},
|
55
|
+
{pathinfo, prep(ServerOptions#sconf.docroot)},
|
56
|
+
{postdata, Request#arg.clidata}}. % TODO: prepare request for railsification
|
57
|
+
|
58
|
+
decode_result({response, ResponseData}) ->
|
59
|
+
convert_response(ResponseData).
|
60
|
+
|
61
|
+
convert_response(EhtmlTuple) ->
|
62
|
+
{Status, AllHeaders, Html} = EhtmlTuple,
|
63
|
+
{allheaders, HeaderList} = AllHeaders,
|
64
|
+
ProcessedHeaderList = lists:map(fun({header, Name, Value}) -> {header, [binary_to_list(Name) ++ ":", binary_to_list(Value)]} end,
|
65
|
+
tuple_to_list(HeaderList)),
|
66
|
+
{html, RawResult} = Html,
|
67
|
+
[Status, {allheaders, ProcessedHeaderList}, {html, binary_to_list(RawResult)}].
|
68
|
+
|
69
|
+
prep(A) when is_list(A) -> list_to_binary(A);
|
70
|
+
prep(A) -> A.
|
71
|
+
|
72
|
+
convert_method(Request) ->
|
73
|
+
R = Request#arg.req,
|
74
|
+
{http_request,Method,{_Type,_Path},_} = R,
|
75
|
+
{method, Method}.
|
76
|
+
|
77
|
+
convert_querypath(Request) ->
|
78
|
+
R = Request#arg.req,
|
79
|
+
{http_request,_Method,{_Type,Path},_} = R,
|
80
|
+
{querypath, prep(Path)}.
|
81
|
+
|
82
|
+
convert_version(Request) ->
|
83
|
+
R = Request#arg.req,
|
84
|
+
{http_request,_Method,{_Type,_Path},Version} = R,
|
85
|
+
{http_version, Version}.
|
86
|
+
|
87
|
+
convert_req(R) ->
|
88
|
+
{http_request,Method,{_Type,Path},_} = R,
|
89
|
+
{Method, prep(Path)}.
|
90
|
+
|
91
|
+
convert_headers(A) ->
|
92
|
+
NormalHeaders = [{connection, prep(A#headers.connection)},
|
93
|
+
{accept, prep(A#headers.accept)},
|
94
|
+
{host, prep(A#headers.host)},
|
95
|
+
{if_modified_since, prep(A#headers.if_modified_since)},
|
96
|
+
{if_match, prep(A#headers.if_match)},
|
97
|
+
{if_none_match, prep(A#headers.if_none_match)},
|
98
|
+
{if_range, prep(A#headers.if_range)},
|
99
|
+
{if_unmodified_since, prep(A#headers.if_unmodified_since)},
|
100
|
+
{range, prep(A#headers.range)},
|
101
|
+
{referer, prep(A#headers.referer)},
|
102
|
+
{user_agent, prep(A#headers.user_agent)},
|
103
|
+
{accept_ranges, prep(A#headers.accept_ranges)},
|
104
|
+
{keep_alive, prep(A#headers.keep_alive)},
|
105
|
+
{location, prep(A#headers.location)},
|
106
|
+
{content_length, prep(A#headers.content_length)},
|
107
|
+
{content_type, prep(A#headers.content_type)},
|
108
|
+
{content_encoding, prep(A#headers.content_encoding)},
|
109
|
+
{authorization, prep(A#headers.authorization)},
|
110
|
+
{transfer_encoding, prep(A#headers.transfer_encoding)}],
|
111
|
+
SpecialHeaders = lists:map(fun({http_header, _Len, Name, _, Value}) -> {prep(Name), prep(Value)} end, A#headers.other),
|
112
|
+
list_to_tuple([{Name, Res} || {Name, Res} <- NormalHeaders, Res /= undefined] ++ SpecialHeaders).
|
Binary file
|
@@ -0,0 +1,108 @@
|
|
1
|
+
%%%-------------------------------------------------------------------
|
2
|
+
%%% File : /Users/dfayram/Projects/concilium/elibs/resource_manager.erl
|
3
|
+
%%% Author : David Fayram
|
4
|
+
%%%-------------------------------------------------------------------
|
5
|
+
-module(resource_manager).
|
6
|
+
-behaviour(gen_server).
|
7
|
+
|
8
|
+
%% API exports
|
9
|
+
-export([start_link/3, start/3,nodes/0,nodecount/0,change_nodecount/1,cycle/0]).
|
10
|
+
|
11
|
+
%% gen_server callback exports
|
12
|
+
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
13
|
+
terminate/2, code_change/3]).
|
14
|
+
|
15
|
+
%% Erlang records are ugly.
|
16
|
+
-record(state, {generator = fun() -> undefined end,
|
17
|
+
terminator = fun(_) -> undefined end,
|
18
|
+
nodecount = 4,
|
19
|
+
nodes = [],
|
20
|
+
term_hook = fun(_) -> undefined end
|
21
|
+
}).
|
22
|
+
|
23
|
+
%% External call functions
|
24
|
+
|
25
|
+
% Note the local server, one of these should run on every
|
26
|
+
% node serving up rails responders.
|
27
|
+
start_link(Generator, Terminator, NumNodes) ->
|
28
|
+
gen_server:start_link({local, ?MODULE}, ?MODULE, [Generator, Terminator, NumNodes], []).
|
29
|
+
start(Generator, Terminator, NumNodes) ->
|
30
|
+
gen_server:start({local, ?MODULE}, ?MODULE, [Generator, Terminator, NumNodes], []).
|
31
|
+
|
32
|
+
nodes() -> gen_server:call(?MODULE, nodes).
|
33
|
+
nodecount() -> gen_server:call(?MODULE, nodecount).
|
34
|
+
change_nodecount(NewNodecount) -> gen_server:cast(?MODULE, {change_nodecount, NewNodecount}).
|
35
|
+
cycle() -> gen_server:cast(?MODULE, cycle).
|
36
|
+
|
37
|
+
%% GEN_SERVER callbacks.
|
38
|
+
init([Generator, Terminator, NumNodes]) ->
|
39
|
+
process_flag(trap_exit, true),
|
40
|
+
Nodes = spawn_nodes(Generator, NumNodes),
|
41
|
+
{ok, #state{generator = Generator, nodecount = NumNodes,
|
42
|
+
nodes = Nodes, terminator = Terminator}}.
|
43
|
+
|
44
|
+
handle_call(term_hook, _From, State) ->
|
45
|
+
{reply, State#state.term_hook, State};
|
46
|
+
handle_call({term_hook, Hook}, _From, State) when is_function(Hook, 1) ->
|
47
|
+
{reply, State#state.term_hook, State#state{term_hook = Hook}};
|
48
|
+
handle_call(nodecount,_From,State) ->
|
49
|
+
{reply, State#state.nodecount, State};
|
50
|
+
handle_call(nodes, _From, State) ->
|
51
|
+
{reply, State#state.nodes, State}.
|
52
|
+
|
53
|
+
handle_cast(cycle, State) ->
|
54
|
+
drop_nodes(State#state.terminator, State#state.nodes),
|
55
|
+
{noreply, State#state{nodes=spawn_nodes(State#state.generator, State#state.nodecount)}};
|
56
|
+
handle_cast({change_nodecount, NewCount}, S) when is_number(NewCount) ->
|
57
|
+
Count = S#state.nodecount,
|
58
|
+
if
|
59
|
+
NewCount > Count ->
|
60
|
+
{noreply,
|
61
|
+
S#state{nodecount = NewCount,
|
62
|
+
nodes = spawn_nodes(S#state.generator, NewCount - Count) ++ S#state.nodes}};
|
63
|
+
NewCount < Count ->
|
64
|
+
{ToKill, ToKeep} = lists:split(NewCount - Count, S#state.nodes),
|
65
|
+
drop_nodes(S#state.terminator, ToKill),
|
66
|
+
{noreply, S#state{nodecount=NewCount, nodes=ToKeep}};
|
67
|
+
true ->
|
68
|
+
{noreply, S}
|
69
|
+
end.
|
70
|
+
|
71
|
+
handle_info({'EXIT', Pid, _Reason}, S) ->
|
72
|
+
Term = S#state.terminator,
|
73
|
+
Membership = lists:any(fun(X) -> X =:= Pid end, S#state.nodes),
|
74
|
+
if
|
75
|
+
Membership ->
|
76
|
+
Term(Pid),
|
77
|
+
Res = lists:delete(Pid, S#state.nodes),
|
78
|
+
NewNode = spawn_linked_node(S#state.generator),
|
79
|
+
{noreply, S#state{nodes=[NewNode|Res]}};
|
80
|
+
true ->
|
81
|
+
{noreply, S}
|
82
|
+
end;
|
83
|
+
handle_info(Any,S) ->
|
84
|
+
io:format("Got INFO ~p~n", [Any]),
|
85
|
+
{noreply, S}.
|
86
|
+
|
87
|
+
terminate(_Reason, _State) ->
|
88
|
+
ok.
|
89
|
+
|
90
|
+
code_change(_OldVsn, State, _Extra) ->
|
91
|
+
{ok, State}.
|
92
|
+
|
93
|
+
%% Utility functions
|
94
|
+
|
95
|
+
spawn_linked_node(Generator) ->
|
96
|
+
Node = Generator(),
|
97
|
+
link(Node),
|
98
|
+
Node.
|
99
|
+
|
100
|
+
spawn_nodes(Generator,NumNodes) ->
|
101
|
+
spawn_nodes(Generator,NumNodes,[]).
|
102
|
+
|
103
|
+
spawn_nodes(_Generator,0,Acc) -> Acc;
|
104
|
+
spawn_nodes(Generator,NumNodes,Acc) -> spawn_nodes(Generator,NumNodes - 1, [spawn_linked_node(Generator)|Acc]).
|
105
|
+
|
106
|
+
drop_nodes(Terminator, Nodes) ->
|
107
|
+
Killer = fun(Node) -> unlink(Node), Terminator(Node) end,
|
108
|
+
lists:map(Killer, Nodes).
|