-module(dasherl_gunicorn_worker). -behaviour(gen_server). %% API -export([start_link/1, stop/1, run_server/1, setup_callback/4]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("dasherl.hrl"). % the values can be override during initialization -record(state, {py_pid = undefined :: pid(), mon_ref = undefined, workers = 3 :: non_neg_integer(), bind = "127.0.0.1:80" :: string(), gunicorn_pid = undefined :: pid()}). start_link(Settings) -> gen_server:start_link(?MODULE, Settings, []). run_server(Pid) -> gen_server:call(Pid, run_server). stop(Pid) -> gen_server:call(Pid, stop_link). setup_callback(Pid, Outputs, Inputs, Bind) -> gen_server:call(Pid, {setup_callback, Outputs, Inputs, Bind}). init(Settings) -> process_flag(trap_exit, true), lager:debug("starting gunicorn worker with settings: ~p", [Settings]), % actually settings can be set from config but also can be passed as an % argument of init gen_server Workers = proplists:get_value(workers, Settings, ?DEFAULT_WORKERS), Bind = proplists:get_value(bind, Settings, ?DEFAULT_BIND), Stylesheets = proplists:get_value(stylesheets, Settings, ?DEFAULT_STYLESHEETS), Path = code:priv_dir(dasherl), % start the py process and initializes its importing modules case python:start([{python_path, Path}]) of {ok, PyPid} -> MonRef = erlang:monitor(process, PyPid), % initialize gunicorn with dasherl and hold in a process ok = initialize_from_scratch(PyPid, Workers, Bind, Stylesheets), {ok, #state{py_pid = PyPid, mon_ref = MonRef, workers = Workers, bind = Bind}}; Error -> lager:error("cannot initializes py due to ~p", [Error]), {stop, Error} end. handle_call({setup_callback, Outputs, Inputs, Bind}, _From, State) -> % @WARNING this only should work when gunicorn is not running, so setup MUST be called % before run server. PyPid = State#state.py_pid, case catch python:call(PyPid, dasherl, setup_callback, [Outputs, Inputs, Bind]) of {'EXIT', {{python, Class, Argument, _Stack}, _}} -> {reply, {error, {Class, Argument}}, State}; "ok" -> {reply, ok, State} end; handle_call(run_server, _From, State) -> PyPid = State#state.py_pid, Appid = make_pidfile(), % from python side this call blocks the entire port, so hold in a separate % process and keep this gen_server normal state PidGunicorn = spawn_link(python, call, [PyPid, dasherl, run, [Appid]]), {reply, ok, State#state{gunicorn_pid = PidGunicorn}}; handle_call(stop_link, _From, State) -> {stop, normal, ok, State}; handle_call(_Request, _From, State) -> {reply, ok, State}. handle_cast(_Request, State) -> {noreply, State}. handle_info({'DOWN', MonRef, _Type, _Object, _Info}, State=#state{mon_ref = MonRef}) -> % process py pid is down, which one is the process to restart? {noreply, State}; handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, State) -> % when finish process just stop py_pid ok = python:stop(State#state.py_pid), ok = stop_signal(), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %% =================================== %% Internal Funcionts %% =================================== % initialize the app and keep it in python side so we % can run after the entire gen_server was initialized correctly initialize_from_scratch(PyPid, Workers, Bind, Stylesheets) -> BindAtom = list_to_atom(Bind), Appid = make_pidfile(), case catch python:call(PyPid, dasherl, initialize, [Workers, BindAtom, Stylesheets, Appid]) of {'EXIT', {{python, Class, Argument, _Stack}, _}} -> {error, {Class, Argument}}; "ok" -> ok end. % since is a blocking process, gunicorn cannot be stopped just with py % so stop process linked, after that stop with signal handler using % sigterm. stop_signal() -> StrSelf = binary_to_list(make_pidfile()), [UnixPid|_] = string:tokens(os:cmd("cat " ++ ?DEFAULT_UNIX_PID(StrSelf)), "\n"), lager:info("preparing to kill unixpid for gunicorn at ~p~n", UnixPid), _ = os:cmd("kill -9 " ++ UnixPid), _ = file:delete(?DEFAULT_UNIX_PID(StrSelf)), ok. make_pidfile() -> Self = pid_to_list(self()), SelfN = re:replace(Self, "^<|>$", "", [{return, list}, global]), list_to_binary(SelfN).