Add bcrypt_pool for pooling a set of 'worker' port processes
diff --git a/.gitignore b/.gitignore
index 061d5ee..b43c9fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
priv/bcrypt
priv/bcrypt_nif.so
logs/*
+.eunit
## libtool
.deps
diff --git a/Makefile b/Makefile
index 8541698..c7371a9 100644
--- a/Makefile
+++ b/Makefile
@@ -1,10 +1,22 @@
+CC = gcc
+CFLAGS = -g -O2 -Wall
+ERLANG_LIBS = -lerl_interface -lei -lpthread
+
+BCRYPTLIBS = c_src/bcrypt_port.o c_src/bcrypt.o c_src/blowfish.o
+
all: compile
+compile_port: priv/bcrypt
+
+priv/bcrypt: $(BCRYPTLIBS)
+ $(CC) $(CFLAGS) $(BCRYPTLIBS) $(ERLANG_LIBS) -o $@
+
compile:
@ ./rebar compile
tests:
@ ./rebar eunit
-
+
clean:
- @ ./rebar clean
\ No newline at end of file
+ @ ./rebar clean
+
diff --git a/rebar.config b/rebar.config
index f7aa888..72a0f86 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,8 +1,8 @@
%% -*- mode: erlang;erlang-indent-level: 2;indent-tabs-mode: nil -*-
-{so_specs,
- [{"priv/bcrypt_nif.so",
- ["c_src/blowfish.c", "c_src/bcrypt.c", "c_src/bcrypt_nif.c"]},
- {"priv/bcrypt",
- ["c_src/blowfish.c", "c_src/bcrypt.c", "c_src/bcrypt_port.c"]}]}.
+{so_name, "bcrypt_nif.so"}.
+{port_sources, ["c_src/blowfish.c", "c_src/bcrypt.c", "c_src/bcrypt_nif.c"]}.
{erl_opts, [debug_info]}.
+
+{pre_hooks, [{clean, "rm -f priv/bcrypt c_src/bcrypt_port.o"},
+ {compile, "make compile_port"}]}.
diff --git a/src/bcrypt.app.src b/src/bcrypt.app.src
index 8620680..ad759e4 100644
--- a/src/bcrypt.app.src
+++ b/src/bcrypt.app.src
@@ -10,7 +10,7 @@
{default_log_rounds, 12},
% Mechanism to use 'nif' or 'port'
- {mechanism, nif},
+ {mechanism, port},
% Size of port program pool
{pool_size, 4}
diff --git a/src/bcrypt_pool.erl b/src/bcrypt_pool.erl
new file mode 100644
index 0000000..3b72be0
--- /dev/null
+++ b/src/bcrypt_pool.erl
@@ -0,0 +1,81 @@
+%% Copyright (c) 2011 Hunter Morris
+%% Distributed under the MIT license; see LICENSE for details.
+-module(bcrypt_pool).
+-author('Hunter Morris <huntermorris@gmail.com>').
+
+-behaviour(gen_server).
+
+-export([start_link/0, available/1]).
+-export([gen_salt/0, gen_salt/1]).
+-export([hashpw/2]).
+
+%% gen_server
+-export([init/1, code_change/3, terminate/2,
+ handle_call/3, handle_cast/2, handle_info/2]).
+
+-record(state, {
+ size = 0,
+ busy = 0,
+ requests = queue:new(),
+ ports = queue:new()
+ }).
+
+-record(req, {mon, from}).
+
+start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+available(Pid) -> gen_server:cast(?MODULE, {available, Pid}).
+
+gen_salt() -> do_call(fun bcrypt_port:gen_salt/1, []).
+gen_salt(Rounds) -> do_call(fun bcrypt_port:gen_salt/2, [Rounds]).
+hashpw(Password, Salt) -> do_call(fun bcrypt_port:hashpw/3, [Password, Salt]).
+
+init([]) ->
+ {ok, Size} = application:get_env(bcrypt, pool_size),
+ {ok, #state{size = Size}}.
+
+terminate(shutdown, _) -> ok.
+
+handle_call(request, {RPid, _} = From, #state{ports = P} = State) ->
+ case queue:out(P) of
+ {empty, P} ->
+ #state{size = Size, busy = B, requests = R} = State,
+ B1 =
+ if Size > B ->
+ {ok, _} = bcrypt_port_sup:start_child(),
+ B + 1;
+ true ->
+ B
+ end,
+ RRef = erlang:monitor(process, RPid),
+ R1 = queue:in(#req{mon = RRef, from = From}, R),
+ {noreply, State#state{requests = R1,
+ busy = B1}};
+ {{value, PPid}, P1} ->
+ #state{busy = B} = State,
+ {reply, {ok, PPid}, State#state{busy = B + 1, ports = P1}}
+ end;
+handle_call(Msg, _, _) -> exit({unknown_call, Msg}).
+
+handle_cast(
+ {available, Pid},
+ #state{requests = R, ports = P, busy = B} = S) ->
+ case queue:out(R) of
+ {empty, R} ->
+ {noreply, S#state{ports = queue:in(Pid, P), busy = B - 1}};
+ {{value, #req{mon = Mon, from = F}}, R1} ->
+ true = erlang:demonitor(Mon, [flush]),
+ gen_server:reply(F, {ok, Pid}),
+ {noreply, S#state{requests = R1}}
+ end;
+handle_cast(Msg, _) -> exit({unknown_cast, Msg}).
+
+handle_info({'DOWN', Ref, process, _Pid, _Reason}, #state{requests = R} = State) ->
+ R1 = queue:from_list(lists:keydelete(Ref, #req.mon, queue:to_list(R))),
+ {noreply, State#state{requests = R1}};
+handle_info(Msg, _) -> exit({unknown_info, Msg}).
+code_change(_OldVsn, State, _Extra) -> {ok, State}.
+
+do_call(F, Args0) ->
+ {ok, Pid} = gen_server:call(?MODULE, request, infinity),
+ Args = [Pid|Args0],
+ apply(F, Args).
diff --git a/src/bcrypt_port.erl b/src/bcrypt_port.erl
new file mode 100644
index 0000000..90ca174
--- /dev/null
+++ b/src/bcrypt_port.erl
@@ -0,0 +1,108 @@
+%% Copyright (c) 2011 Hunter Morris
+%% Distributed under the MIT license; see LICENSE for details.
+-module(bcrypt_port).
+-author('Hunter Morris <hunter.morris@smarkets.com>').
+
+-behaviour(gen_server).
+
+%% API
+-export([start_link/0, stop/0]).
+-export([gen_salt/1, gen_salt/2]).
+-export([hashpw/3]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
+ code_change/3]).
+
+-record(state, {port, default_log_rounds}).
+
+-define(CMD_SALT, 0).
+-define(CMD_HASHPW, 1).
+-define(BCRYPT_ERROR(F, D), error_logger:error_msg(F, D)).
+-define(BCRYPT_WARNING(F, D), error_logger:warning_msg(F, D)).
+
+start_link() ->
+ Dir = case code:priv_dir(bcrypt) of
+ {error, bad_name} ->
+ case code:which(bcrypt) of
+ Filename when is_list(Filename) ->
+ filename:join(
+ [filename:dirname(Filename), "../priv"]);
+ _ ->
+ "../priv"
+ end;
+ Priv -> Priv
+ end,
+ Port = filename:join(Dir, "bcrypt"),
+ gen_server:start_link(?MODULE, [Port], []).
+
+stop() -> gen_server:call(?MODULE, stop).
+
+gen_salt(Pid) ->
+ R = crypto:rand_bytes(16),
+ gen_server:call(Pid, {encode_salt, R}, infinity).
+
+gen_salt(Pid, LogRounds) ->
+ R = crypto:rand_bytes(16),
+ gen_server:call(Pid, {encode_salt, R, LogRounds}, infinity).
+
+hashpw(Pid, Password, Salt) ->
+ gen_server:call(Pid, {hashpw, Password, Salt}, infinity).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+init([Filename]) ->
+ case file:read_file_info(Filename) of
+ {ok, _Info} ->
+ Port = open_port(
+ {spawn, Filename}, [{packet, 2}, binary, exit_status]),
+ ok = bcrypt_pool:available(self()),
+ {ok, Rounds} = application:get_env(bcrypt, default_log_rounds),
+ {ok, #state{port = Port, default_log_rounds = Rounds}};
+ {error, Reason} ->
+ ?BCRYPT_ERROR("Can't open file ~p: ~p", [Filename, Reason]),
+ {stop, error_opening_bcrypt_file}
+ end.
+
+terminate(_Reason, #state{port=Port}) ->
+ catch port_close(Port),
+ ok.
+
+handle_call({encode_salt, R}, From, #state{default_log_rounds = LogRounds} = State) ->
+ handle_call({encode_salt, R, LogRounds}, From, State);
+handle_call({encode_salt, R, LogRounds}, From, State) ->
+ Port = State#state.port,
+ Data = term_to_binary({?CMD_SALT, From, {R, LogRounds}}),
+ port_command(Port, Data),
+ {noreply, State};
+handle_call({hashpw, Password, Salt}, From, State) ->
+ Port = State#state.port,
+ Data = term_to_binary({?CMD_HASHPW, From, {Password, Salt}}),
+ port_command(Port, Data),
+ {noreply, State};
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(Msg, _, _) -> exit({unknown_call, Msg}).
+
+handle_cast(Msg, _) -> exit({unknown_cast, Msg}).
+
+code_change(_OldVsn, State, _Extra) -> {ok, State}.
+
+handle_info({Port, {data, Data}}, #state{port=Port}=State) ->
+ {Cmd, To, Reply0} = binary_to_term(Data),
+ Reply =
+ case Reply0 of
+ "Invalid salt" -> {error, invalid_salt};
+ "Invalid salt length" -> {error, invalid_salt_length};
+ "Invalid number of rounds" -> {error, invalid_rounds};
+ _ -> {ok, Reply0}
+ end,
+ gen_server:reply(To, Reply),
+ ok = bcrypt_pool:available(self()),
+ {noreply, State};
+handle_info({Port, {exit_status, Status}}, #state{port=Port}=State) ->
+ %% Rely on whomever is supervising this process to restart.
+ ?BCRYPT_WARNING("Port died: ~p", [Status]),
+ {stop, port_died, State};
+handle_info(Msg, _) -> exit({unknown_info, Msg}).
diff --git a/src/bcrypt_port_sup.erl b/src/bcrypt_port_sup.erl
index 61752e0..451751f 100644
--- a/src/bcrypt_port_sup.erl
+++ b/src/bcrypt_port_sup.erl
@@ -5,9 +5,10 @@
-behaviour(supervisor).
--export([start_link/0, init/1]).
+-export([start_link/0, start_child/0, init/1]).
start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+start_child() -> supervisor:start_child(?MODULE, []).
init([]) ->
{ok, {{simple_one_for_one, 1, 1},