Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial import

  • Loading branch information...
commit b865a73b149b5417fe0cd137eb15b158cbe9820a 0 parents
Andy Skelton authored
22 php.app
@@ -0,0 +1,22 @@
+{application, php,
+ [
+ {mod,
+ {php_app,
+ [
+ %% php processes to use
+ %% default: erlang:system_info(logical_processors)
+ % {procs, 2},
+ {opts,[
+ %% path to PHP CLI binary
+ %{php, "/usr/local/bin/php"},
+ %% working dir for PHP (docroot?)
+ %{dir, "/home/skeltoac/public_html"},
+ %% initial PHP commands (includes?)
+ %{init, "require('wp-config.php');"},
+ %% default maximum memory allowed (Kib or infinity)
+ {maxmem, 102400}
+ ]}]
+ }
+ }
+ ]
+}.
345 php.erl
@@ -0,0 +1,345 @@
+%%%-------------------------------------------------------------------
+%%% File : php.erl
+%%% Author : Andy Skelton <andy@automattic.com>
+%%% Purpose : Provides API for evaluating PHP code.
+%%% Created : 15 Jan 2009 by Andy Skelton <andy@automattic.com>
+%%%
+%%% Copyright (C) 2009 Automattic
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%-------------------------------------------------------------------
+
+%% @author Andy Skelton <andy@automattic.com>
+%% [http://andy.wordpress.com/]
+%% @doc This module provides all of the API functions and a gen_server
+%% that marshals requests for PHP code evaluation. It maintains
+%% queues of available and reserved PHP processes and serves each
+%% client request in order. PHP processes are reused to cut down
+%% on startup overhead and memory limits are checked after each
+%% PHP code evaluation to help prevent resource hogging.
+%%
+%% Configuration is done in the php.app file. Options include the
+%% path to the PHP binary, code to initialize the environment,
+%% and a default memory limit. It is also possible to set the
+%% number of concurrent PHP instances; the default is the number
+%% of logical processors available to the Erlang VM.
+-module(php).
+
+-behaviour(gen_server).
+
+%% API
+-export([
+ start/0,
+ stop/0,
+ start_link/0,
+ eval/1,eval/2,eval/3,
+ reserve/0,reserve/1,
+ release/1,
+ get_mem/1,
+ restart_all/0
+ ]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-record(state, {
+ sup,
+ free = [],
+ reserved = [],
+ waiting = [],
+ restart
+ }).
+
+-record(php, {
+ ref,
+ pid,
+ maxmem
+ }).
+
+-record(restart, {
+ pids = [],
+ froms = []
+ }).
+
+%%====================================================================
+%% API
+%%====================================================================
+
+%% @spec start() -> ok
+%% @doc Starts the PHP application, supervisor, a number of workers,
+%% and this API server module with the options set in php.app.
+start() ->
+ application:start(php).
+
+%% @spec stop() -> ok
+%% @doc Stops the PHP application and everything it started.
+stop() ->
+ application:stop(php).
+
+%% @private
+%% @spec start_link() -> {ok, state()}
+%% @doc An entry point for the supervisor. This calls init(Pid) with
+%% the pid of the supervisor so we can discover the workers.
+start_link() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, self(), []).
+
+%% @spec eval(Code) -> result()
+%% Code = list() | binary()
+%% @type result() = ( {ok, Output, Return, Error, Status} |
+%% {parse_error, Error, Status} |
+%% {exit, ExitCode | timeout} )
+%% Output = binary()
+%% Return = any()
+%% Error = binary()
+%% Status = continue | break
+%% ExitCode = integer()
+%% @doc Equivalent to eval(Code, undefined, infinity).
+eval(Code) ->
+ eval(Code, undefined, infinity).
+
+%% @spec eval(Code, (reference() | Timeout)) -> result()
+%% Timeout = integer() | infinity
+%% @doc Equivalent to eval(Code, Php, infinity) or eval(Code,
+%% undefined, Timeout).
+eval(Code, Ref) when is_reference(Ref) ->
+ eval(Code, Ref, infinity);
+eval(Code, Timeout) when is_integer(Timeout), Timeout > 0; Timeout =:= infinity ->
+ eval(Code, undefined, Timeout);
+eval(_, _) ->
+ {error, invalid_argument}.
+
+%% @spec eval(Code, Ref, Timeout) -> result() | {error, term()}
+%% Code = list() | binary()
+%% Ref = reference() | undefined
+%% Timeout = integer() | infinity
+%% @doc Tests syntax and evaluates PHP code.
+%% A parse error will result in {parse_error, Error, Status}.
+%% A fatal error or exit() will result in {exit, ExitCode}.
+%% On success, Error contains the last error message. You may
+%% call trigger_error() to store a string here but an error or
+%% warning (subject to error_reporting()) will overwrite it.
+%% Status indicates whether the PHP process continued or broke
+%% after evaluation. This suggests that any variables you set
+%% in a reserved PHP persist but that can not be guaranteed.
+eval(Code, Ref, Timeout) ->
+ gen_server:call(?MODULE, {eval, Code, Ref, Timeout}, infinity).
+
+%% @spec reserve() -> reference()
+%% @doc Equivalent to reserve(undefined).
+reserve() ->
+ reserve(undefined).
+
+%% @spec reserve(MaxMem) -> reference()
+%% MaxMem = integer() | infinity | undefined
+%% @doc Reserves a PHP instance that will only be accessible to
+%% callers possessing the key returned by this function. A
+%% reservation can be passed around or held indefinitely.
+%% MaxMem is in KiB. PHP size is measured by `'ps -o rss`'
+%% after each evaluation and if it exceeds MaxMem, the PHP
+%% instance is restarted and the returned Status is break.
+%% If MaxMem is undefined, it becomes the value in php.app.
+reserve(MaxMem) ->
+ gen_server:call(?MODULE, {reserve, MaxMem, ref}, infinity).
+
+%% @spec release(reference()) -> ok
+%% @doc Cancels the reservation of a PHP instance, returning it
+%% to the pool of available instances.
+release(Ref) ->
+ gen_server:cast(?MODULE, {release, Ref}).
+
+%% @spec get_mem(reference()) -> integer() | {error, term()}
+%% @doc Measures the memory footprint of the PHP instance using
+%% `'ps -o rss`'. If the instance has died, it is restarted
+%% before the measurement is taken.
+get_mem(Ref) ->
+ gen_server:call(?MODULE, {get_mem, Ref}, infinity).
+
+%% @spec restart_all() -> ok
+%% @doc Restarts each PHP thread, waiting if any are reserved. This
+%% is intended to force an updated PHPLOOP into use.
+restart_all() ->
+ gen_server:call(?MODULE, restart_all, infinity).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+
+%% @private
+init(From) ->
+ process_flag(trap_exit,true),
+ State=#state{sup=From,restart=#restart{}},
+ {ok, State}.
+
+%% @private
+handle_call(get_state, _From, State) ->
+ {reply, State, State};
+handle_call({eval, Code, Ref, Timeout}, From, State) ->
+ Php = if
+ Ref =:= undefined ->
+ undefined;
+ true ->
+ find_php(Ref, State#state.reserved)
+ end,
+ if
+ Php =:= none ->
+ {reply, {error, invalid_reservation}, State};
+ true ->
+ spawn_link( fun () -> do_eval(Code, Php, From, Timeout) end ),
+ {noreply, State}
+ end;
+handle_call({reserve, MaxMem, What}, From, State) ->
+ case State#state.waiting of
+ [] -> % no processes waiting for php reservation
+ case {State#state.free, State#state.reserved} of
+ {[],[]} -> % php_eval workers undiscovered (first call to reserve)
+ [Pid|Free] = lists:foldl(
+ fun ({_,Pid,_,[php_eval]}, Acc)->[Pid|Acc];
+ (_, Acc) -> Acc
+ end,
+ [],
+ supervisor:which_children(State#state.sup)
+ ),
+ Php = make_php(Pid,MaxMem),
+ {reply, make_reply(Php, What), State#state{free=Free, reserved=[Php]}};
+ {[],_} -> % all php_eval workers are reserved
+ Waiting = [{From,MaxMem,What}],
+ {noreply, State#state{waiting=Waiting}};
+ {[Pid|Free],_} -> % at least one php_eval worker is free
+ Php = make_php(Pid,MaxMem),
+ Reserved = [Php|State#state.reserved],
+ {reply, make_reply(Php, What), State#state{free=Free, reserved=Reserved}}
+ end;
+ _ -> % processes are already waiting
+ Waiting = State#state.waiting++[{From,MaxMem,What}],
+ {noreply, State#state{waiting=Waiting}}
+ end;
+handle_call({get_mem, Ref}, From, State) ->
+ case find_php(Ref, State#state.reserved) of
+ none -> {reply, {error, invalid_reservation}, State};
+ Php ->
+ spawn_link( fun () -> do_get_mem(Php, From) end ),
+ {noreply, State}
+ end;
+handle_call(restart_all, From, State) ->
+ Froms = (State#state.restart)#restart.froms,
+ Pids = all_pids(State),
+ [spawn(fun()->eval(";")end) || _ <- lists:seq(1,length(Pids))],
+ {noreply, State#state{restart=#restart{froms=[From|Froms],pids=Pids}}};
+handle_call(Request, _From, State) ->
+ {reply, {unknown_call, Request}, State}.
+
+%% @private
+handle_cast({release, Ref}, State) ->
+ case find_php(Ref, State#state.reserved) of
+ none ->
+ {noreply, State};
+ Php ->
+ State2 = maybe_restart(Php#php.pid, State),
+ State3 = do_release(Php, State2),
+ {noreply, State3}
+ end;
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+%% @private
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%% @private
+terminate(_Reason, _State) ->
+ ok.
+
+%% @private
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+
+reserve_php() ->
+ gen_server:call(?MODULE, {reserve, undefined, php}, infinity).
+
+maybe_restart(Pid, #state{restart=#restart{froms=Froms,pids=Pids}}=State) ->
+ case lists:member(Pid, Pids) of
+ false ->
+ State;
+ true ->
+ gen_server:call(Pid, {eval, "exit;", 1, infinity}, infinity),
+ Pids2 = lists:delete(Pid, Pids),
+ if
+ Pids2 =:= [] ->
+ Restart = #restart{},
+ lists:foreach(
+ fun (From) -> gen_server:reply(From, ok) end,
+ Froms
+ );
+ true ->
+ Restart = #restart{froms=Froms,pids=Pids2}
+ end,
+ State#state{restart=Restart}
+ end.
+
+do_release(Php, State) ->
+ Reserved = lists:delete(Php, State#state.reserved),
+ Free = State#state.free ++ [Php#php.pid],
+ case State#state.waiting of
+ [] -> % no processes in the queue
+ State#state{reserved=Reserved, free=Free};
+ [{From,MaxMem,What}|Waiting] -> % there is a process waiting for a reservation
+ [Pid|NewFree] = Free,
+ NextPhp=make_php(Pid,MaxMem),
+ gen_server:reply(From, make_reply(NextPhp, What)),
+ State#state{waiting=Waiting, reserved=[NextPhp|Reserved], free=NewFree}
+ end.
+
+do_eval(Code, #php{pid=Pid,maxmem=MaxMem}, From, Timeout) ->
+ Reply = gen_server:call(Pid, {eval, Code, Timeout, MaxMem}, infinity),
+ gen_server:reply(From, Reply);
+do_eval(Code, _, From, Timeout) ->
+ Php = reserve_php(),
+ do_eval(Code, Php, From, Timeout),
+ release(Php#php.ref).
+
+do_get_mem(#php{pid=Pid}, From) ->
+ Mem = gen_server:call(Pid, get_mem),
+ gen_server:reply(From, Mem).
+
+all_pids(#state{free=[], reserved=[]}) ->
+ [];
+all_pids(#state{free=Free,reserved=Reserved}) ->
+ lists:foldl(
+ fun (#php{pid=Pid}, Acc) -> [Pid|Acc] end,
+ Free,
+ Reserved
+ ).
+
+make_php(Pid, MaxMem) ->
+ #php{ref=make_ref(),pid=Pid,maxmem=MaxMem}.
+
+find_php(_, []) ->
+ none;
+find_php(Ref, [#php{ref=Ref}=Php|_]) ->
+ Php;
+find_php(Ref, [_|T]) ->
+ find_php(Ref, T).
+
+make_reply(Php, php) ->
+ Php;
+make_reply(#php{ref=Ref}, ref) ->
+ Ref.
146 php.html
@@ -0,0 +1,146 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+<title>Module php</title>
+<link rel="stylesheet" type="text/css" href="stylesheet.css" title="EDoc">
+</head>
+<body bgcolor="white">
+<div class="navbar"><a name="#navbar_top"></a><table width="100%" border="0" cellspacing="0" cellpadding="2" summary="navigation bar"><tr><td><a href="overview-summary.html" target="overviewFrame">Overview</a></td><td><a href="http://www.erlang.org/"><img src="erlang.png" align="right" border="0" alt="erlang logo"></a></td></tr></table></div>
+<hr>
+
+<h1>Module php</h1>
+<ul class="index"><li><a href="#description">Description</a></li><li><a href="#types">Data Types</a></li><li><a href="#index">Function Index</a></li><li><a href="#functions">Function Details</a></li></ul>This module provides all of the API functions and a gen_server
+that marshals requests for PHP code evaluation.
+
+<p><b>Behaviours:</b> <a href="gen_server.html"><tt>gen_server</tt></a>.</p>
+<p><b>Authors:</b> Andy Skelton (<a href="mailto:andy@automattic.com"><tt>andy@automattic.com</tt></a>) [<em>web site:</em> <tt><a href="http://andy.wordpress.com/" target="_top">http://andy.wordpress.com/</a></tt>].</p>
+
+<h2><a name="description">Description</a></h2><p>This module provides all of the API functions and a gen_server
+that marshals requests for PHP code evaluation. It maintains
+queues of available and reserved PHP processes and serves each
+client request in order. PHP processes are reused to cut down
+on startup overhead and memory limits are checked after each
+PHP code evaluation to help prevent resource hogging.</p>
+
+ Configuration is done in the php.app file. Options include the
+ path to the PHP binary, code to initialize the environment,
+ and a default memory limit. It is also possible to set the
+ number of concurrent PHP instances; the default is the number
+ of logical processors available to the Erlang VM.
+<h2><a name="types">Data Types</a></h2>
+
+<h3 class="typedecl"><a name="type-result">result()</a></h3>
+<p><tt>result() = {ok, Output, Return, Error, Status} | {parse_error, Error, Status} | {exit, ExitCode | timeout}</tt>
+<ul class="definitions"><li><tt>Output = binary()</tt></li>
+<li><tt>Return = any()</tt></li>
+<li><tt>Error = binary()</tt></li>
+<li><tt>Status = continue | break</tt></li>
+<li><tt>ExitCode = integer()</tt></li>
+</ul></p>
+
+
+<h2><a name="index">Function Index</a></h2>
+<table width="100%" border="1" cellspacing="0" cellpadding="2" summary="function index"><tr><td valign="top"><a href="#eval-1">eval/1</a></td><td>Equivalent to eval(Code, undefined, infinity).</td></tr>
+<tr><td valign="top"><a href="#eval-2">eval/2</a></td><td>Equivalent to eval(Code, Php, infinity) or eval(Code,
+ undefined, Timeout).</td></tr>
+<tr><td valign="top"><a href="#eval-3">eval/3</a></td><td>Tests syntax and evaluates PHP code.</td></tr>
+<tr><td valign="top"><a href="#get_mem-1">get_mem/1</a></td><td>Measures the memory footprint of the PHP instance using
+ `ps -o rss`.</td></tr>
+<tr><td valign="top"><a href="#release-1">release/1</a></td><td>Cancels the reservation of a PHP instance, returning it
+ to the pool of available instances.</td></tr>
+<tr><td valign="top"><a href="#reserve-0">reserve/0</a></td><td>Equivalent to reserve(undefined).</td></tr>
+<tr><td valign="top"><a href="#reserve-1">reserve/1</a></td><td>Reserves a PHP instance that will only be accessible to
+ callers possessing the key returned by this function.</td></tr>
+<tr><td valign="top"><a href="#restart_all-0">restart_all/0</a></td><td>Restarts each PHP thread, waiting if any are reserved.</td></tr>
+<tr><td valign="top"><a href="#start-0">start/0</a></td><td>Starts the PHP application, supervisor, a number of workers,
+ and this API server module with the options set in php.app.</td></tr>
+<tr><td valign="top"><a href="#stop-0">stop/0</a></td><td>Stops the PHP application and everything it started.</td></tr>
+</table>
+
+<h2><a name="functions">Function Details</a></h2>
+
+<h3 class="function"><a name="eval-1">eval/1</a></h3>
+<div class="spec">
+<p><tt>eval(Code) -&gt; <a href="#type-result">result()</a></tt>
+<ul class="definitions"><li><tt>Code = list() | binary()</tt></li>
+</ul></p>
+</div><p>Equivalent to eval(Code, undefined, infinity).</p>
+
+<h3 class="function"><a name="eval-2">eval/2</a></h3>
+<div class="spec">
+<p><tt>eval(Code, Ref::reference() | Timeout) -&gt; <a href="#type-result">result()</a></tt>
+<ul class="definitions"><li><tt>Timeout = integer() | infinity</tt></li>
+</ul></p>
+</div><p>Equivalent to eval(Code, Php, infinity) or eval(Code,
+ undefined, Timeout).</p>
+
+<h3 class="function"><a name="eval-3">eval/3</a></h3>
+<div class="spec">
+<p><tt>eval(Code, Ref, Timeout) -&gt; <a href="#type-result">result()</a> | {error, term()}</tt>
+<ul class="definitions"><li><tt>Code = list() | binary()</tt></li>
+<li><tt>Ref = reference() | undefined</tt></li>
+<li><tt>Timeout = integer() | infinity</tt></li>
+</ul></p>
+</div><p>Tests syntax and evaluates PHP code.
+ A parse error will result in {parse_error, Error, Status}.
+ A fatal error or exit() will result in {exit, ExitCode}.
+ On success, Error contains the last error message. You may
+ call trigger_error() to store a string here but an error or
+ warning (subject to error_reporting()) will overwrite it.
+ Status indicates whether the PHP process continued or broke
+ after evaluation. This suggests that any variables you set
+ in a reserved PHP persist but that can not be guaranteed.</p>
+
+<h3 class="function"><a name="get_mem-1">get_mem/1</a></h3>
+<div class="spec">
+<p><tt>get_mem(Ref::reference()) -&gt; integer() | {error, term()}</tt></p>
+</div><p>Measures the memory footprint of the PHP instance using
+ `ps -o rss`. If the instance has died, it is restarted
+ before the measurement is taken.</p>
+
+<h3 class="function"><a name="release-1">release/1</a></h3>
+<div class="spec">
+<p><tt>release(Ref::reference()) -&gt; ok</tt></p>
+</div><p>Cancels the reservation of a PHP instance, returning it
+ to the pool of available instances.</p>
+
+<h3 class="function"><a name="reserve-0">reserve/0</a></h3>
+<div class="spec">
+<p><tt>reserve() -&gt; reference()</tt></p>
+</div><p>Equivalent to reserve(undefined).</p>
+
+<h3 class="function"><a name="reserve-1">reserve/1</a></h3>
+<div class="spec">
+<p><tt>reserve(MaxMem) -&gt; reference()</tt>
+<ul class="definitions"><li><tt>MaxMem = integer() | infinity | undefined</tt></li>
+</ul></p>
+</div><p>Reserves a PHP instance that will only be accessible to
+ callers possessing the key returned by this function. A
+ reservation can be passed around or held indefinitely.
+ MaxMem is in KiB. PHP size is measured by `ps -o rss`
+ after each evaluation and if it exceeds MaxMem, the PHP
+ instance is restarted and the returned Status is break.
+ If MaxMem is undefined, it becomes the value in php.app.</p>
+
+<h3 class="function"><a name="restart_all-0">restart_all/0</a></h3>
+<div class="spec">
+<p><tt>restart_all() -&gt; ok</tt></p>
+</div><p>Restarts each PHP thread, waiting if any are reserved. This
+ is intended to force an updated PHPLOOP into use.</p>
+
+<h3 class="function"><a name="start-0">start/0</a></h3>
+<div class="spec">
+<p><tt>start() -&gt; ok</tt></p>
+</div><p>Starts the PHP application, supervisor, a number of workers,
+ and this API server module with the options set in php.app.</p>
+
+<h3 class="function"><a name="stop-0">stop/0</a></h3>
+<div class="spec">
+<p><tt>stop() -&gt; ok</tt></p>
+</div><p>Stops the PHP application and everything it started.</p>
+<hr>
+
+<div class="navbar"><a name="#navbar_bottom"></a><table width="100%" border="0" cellspacing="0" cellpadding="2" summary="navigation bar"><tr><td><a href="overview-summary.html" target="overviewFrame">Overview</a></td><td><a href="http://www.erlang.org/"><img src="erlang.png" align="right" border="0" alt="erlang logo"></a></td></tr></table></div>
+<p><i>Generated by EDoc, Feb 6 2009, 09:29:57.</i></p>
+</body>
+</html>
18 php_app.erl
@@ -0,0 +1,18 @@
+%%%-------------------------------------------------------------------
+%%% File : php_app.erl
+%%% Author : Andy Skelton <andy@automattic.com>
+%%% Purpose : Helps start php_sup
+%%% Created : 15 Jan 2009 by Andy Skelton <andy@automattic.com>
+%%% License : GPLv2
+%%%-------------------------------------------------------------------
+-module(php_app).
+
+-behaviour(application).
+
+-export([start/2, stop/1]).
+
+start(_Type, StartArgs) ->
+ php_sup:start_link(StartArgs).
+
+stop(_State) ->
+ ok.
194 php_eval.erl
@@ -0,0 +1,194 @@
+%%%-------------------------------------------------------------------
+%%% File : php_eval.erl
+%%% Author : Andy Skelton <andy@automattic.com>
+%%% Purpose : A server for running PHP code.
+%%% Created : 15 Jan 2009 by Andy Skelton <andy@automattic.com>
+%%% License : GPLv2
+%%%-------------------------------------------------------------------
+-module(php_eval).
+
+-behaviour(gen_server).
+
+%% API
+-export([start_link/0, start_link/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-import(php_util,[get_opt/3]).
+
+%% port handler in PHP
+-define(PHPLOOP, "ini_set('track_errors',true);do{ob_start();@$_C_=fread(STDIN,array_pop(unpack('N',fread(STDIN,4))));@trigger_error('');if(eval('return true;'.$_C_)){$_R_=serialize(eval($_C_));}else{$_R_='E;';}$_R_.=serialize($php_errormsg);$_R_.=serialize(ob_get_clean());fwrite(STDOUT,pack('N',strlen($_R_)).$_R_);}while(!empty($_C_));exit;").
+
+-record(state, {
+ port,
+ opts,
+ pid
+ }).
+
+%%====================================================================
+%% API
+%%====================================================================
+start_link() ->
+ start_link([]).
+start_link(Args) ->
+ gen_server:start_link(?MODULE, Args, []).
+
+%%====================================================================
+%% gen_server callbacks
+%%====================================================================
+
+init(Opts) ->
+ process_flag(trap_exit,true),
+ State = start_php(#state{opts=Opts}),
+ {ok, State}.
+
+handle_call(get_state, _, State) ->
+ {reply, State, State};
+handle_call({eval, Code, Timeout, MaxMem}, _, #state{opts=Opts}=OrigState) ->
+ State = guarantee_php(OrigState),
+ Exec = exec_php(State#state.port, Code, Timeout),
+ Limit = if
+ MaxMem =:= undefined -> get_opt(maxmem, Opts, infinity);
+ true -> MaxMem
+ end,
+ case Limit of
+ infinity ->
+ NewState = State;
+ _ ->
+ case get_mem(State#state.pid) of
+ Mem when not is_integer(Mem); Mem > Limit ->
+ NewState = restart_php(State);
+ _ ->
+ NewState = State
+ end
+ end,
+ Reply = if
+ element(1, Exec) =:= exit ->
+ Exec;
+ NewState#state.pid =:= State#state.pid ->
+ erlang:append_element(Exec, continue);
+ true ->
+ erlang:append_element(Exec, break)
+ end,
+ {reply, Reply, NewState};
+handle_call(get_mem, _, OrigState) ->
+ State = guarantee_php(OrigState),
+ Mem = get_mem(State#state.pid),
+ {reply, Mem, State};
+handle_call(restart_php, _, State) ->
+ {reply, ok, restart_php(State)};
+handle_call(_Request, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info({'EXIT', OldPort, _Why}, #state{port=OldPort}=State) ->
+ {noreply, restart_php(State)};
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ stop_php(State#state.port),
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%%--------------------------------------------------------------------
+%%% Internal functions
+%%--------------------------------------------------------------------
+
+start_php(#state{opts=Opts}=State) ->
+ Php = get_opt(php, Opts, "php"),
+ Init = get_opt(init, Opts, []),
+ Dir = get_opt(dir, Opts, []),
+ Envs = get_opt(envs, Opts, []),
+ Command = Php ++ " -r '" ++ escape(Init) ++ ";" ++ escape(?PHPLOOP) ++ "'",
+ PortOpts = [{packet,4},exit_status] ++
+ case Dir of
+ [] -> [];
+ _ -> [{cd, Dir}]
+ end ++
+ case Envs of
+ [] -> [];
+ _ -> [{env, Envs}]
+ end,
+ Port = open_port({spawn, Command}, PortOpts),
+ Pid = get_pid(Port),
+ State#state{port=Port,pid=Pid}.
+
+stop_php(Port) ->
+ case erlang:port_info(Port) of
+ undefined ->
+ ok;
+ _ ->
+ exec_php(Port, "exit(0);", 0),
+ ok
+ end.
+
+restart_php(State) ->
+ stop_php(State#state.port),
+ start_php(State).
+
+guarantee_php(State) ->
+ case get_pid(State#state.port) of
+ Pid when is_integer(Pid) -> State#state{pid=Pid};
+ _ -> guarantee_php(restart_php(State))
+ end.
+
+get_pid(Port) ->
+ case exec_php(Port, "return getmypid();", 5000) of
+ {_,_,Pid,_} -> Pid;
+ _ -> undefined
+ end.
+
+get_mem(Pid) ->
+ case is_integer(Pid) of
+ true ->
+ Mem = string:strip(string:strip(os:cmd("ps h -o rss "++integer_to_list(Pid))),right,10),
+ case length(Mem) of
+ 0 -> undefined;
+ _ -> list_to_integer(Mem)
+ end;
+ false ->
+ undefined
+ end.
+
+%% @spec (list()) -> list()
+%% @doc Replaces ' with '\'' for use in bash command arguments. Since
+%% it is impossible to escape a single-quote in a single-quoted
+%% argument, we must break out of the quotes before escaping it.
+escape(Str) ->
+ escape(Str, []).
+
+escape([], Acc) ->
+ lists:reverse(Acc);
+escape([H|T], Acc) ->
+ case H =:= 39 of % 39 is single-quote
+ true ->
+ escape(T, [39,39,92,39|Acc]); % 92 is backslash
+ false ->
+ escape(T, [H|Acc])
+ end.
+
+exec_php(Port, Code, Timeout) ->
+ Port ! {self(), {command, list_to_binary(Code)}},
+ receive
+ {Port, {exit_status, Status}} -> {exit, Status};
+ {Port, {data, Data}} -> {Return, Rest} = php_util:unserialize(Data),
+ {Error, Rest2} = php_util:unserialize(Rest),
+ {Output, _End} = php_util:unserialize(Rest2),
+ case Return of
+ error ->
+ {parse_error, Error};
+ _ ->
+ {ok, Output, Return, Error}
+ end
+ after
+ Timeout ->
+ exit(Port, kill),
+ {exit, timeout}
+ end.
53 php_sup.erl
@@ -0,0 +1,53 @@
+%%%-------------------------------------------------------------------
+%%% File : php_sup.erl
+%%% Author : Andy Skelton <andy@automattic.com>
+%%% Purpose : Supervisor for php_app
+%%% Created : 15 Jan 2009 by Andy Skelton <andy@automattic.com>
+%%% License : GPLv2
+%%%-------------------------------------------------------------------
+-module(php_sup).
+
+-behaviour(supervisor).
+
+-import(php_util,[get_opt/3]).
+
+%% API
+-export([start_link/0,start_link/1]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+-define(SERVER, ?MODULE).
+
+%%====================================================================
+%% API
+%%====================================================================
+start_link() ->
+ start_link([]).
+start_link(Args) ->
+ supervisor:start_link({local, ?SERVER}, ?MODULE, Args).
+
+%%====================================================================
+%% Supervisor callbacks
+%%====================================================================
+init(Args) ->
+ Procs = case get_opt(procs, Args, default) of
+ ProcsArg when is_integer(ProcsArg) ->
+ ProcsArg;
+ _ ->
+ erlang:system_info(logical_processors)
+ end,
+ Opts = get_opt(opts, Args, []),
+ Interface = { php, {php, start_link, [] },
+ permanent, 2000, worker, [php] },
+ Servers = [ {get_proc_name(phpeval,P),{php_eval,start_link,[Opts]},
+ permanent,2000,worker,[php_eval]}
+ || P <- lists:seq(1, Procs) ],
+ {ok,{{one_for_all,1,1}, Servers ++ [Interface]}}.
+
+%%====================================================================
+%% Internal functions
+%%====================================================================
+
+get_proc_name(Name, Num) when is_integer(Num) ->
+ list_to_atom(atom_to_list(Name) ++ "_" ++ integer_to_list(Num)).
115 php_util.erl
@@ -0,0 +1,115 @@
+%%%-------------------------------------------------------------------
+%%% File : php.erl
+%%% Author : Richard Jones <rj@last.fm>, Andy Skelton <andy@automattic.com>
+%%% Purpose : Provides API for evaluating PHP code.
+%%% Created : 19 Jan 2009 by Andy Skelton <andy@automattic.com>
+%%% License : GPLv2
+%%%-------------------------------------------------------------------
+
+%%
+%% Takes a serialized php object and turns it into an erlang data structure
+%%
+-module(php_util).
+-author("Richard Jones <rj at last.fm>"). % http://www.metabrew.com/article/reading-serialized-php-objects-from-erlang/ GPLv2
+-author("Andy Skelton <andy at automattic.com>"). % minor tweaks
+-export([unserialize/1]).
+-export([get_opt/3]).
+%% Usage: {Result, Leftover} = php:unserialize(...)
+unserialize(S) when is_binary(S) ->
+ unserialize(binary_to_list(S));
+unserialize(S) when is_list(S) ->
+ {[Result], Rest} = takeval(S, 1),
+ {Result, Rest}.
+
+%% Internal stuff
+
+takeval(Str, Num) ->
+ {Parsed, Remains} = takeval(Str, Num, []),
+ {lists:reverse(Parsed), Remains}.
+
+takeval([$} | Leftover], 0, Acc) -> {Acc, Leftover};
+takeval(Str, 0, Acc) -> {Acc, Str};
+takeval([], 0, Acc) -> Acc;
+
+takeval(Str, Num, Acc) ->
+ {Val, Rest} = phpval(Str),
+ %%Lots of tracing if you enable this:
+ %%io:format("\nState\n Str: ~s\n Num: ~w\n Acc:~w\n", [Str,Num,Acc]),
+ %%io:format("-Val: ~w\n-Rest: ~s\n\n",[Val, Rest]),
+ takeval(Rest, Num-1, [Val | Acc]).
+%%
+%% Parse induvidual php values.
+%% a "phpval" here is T:val; where T is the type code for int, object, array etc..
+%%
+%% Simple ones:
+phpval([]) -> {[],[]};
+phpval([ $} | Rest ]) -> phpval(Rest); % skip }
+phpval([$N,$;|Rest]) -> {null, Rest}; % null
+phpval([$E,$;|Rest]) -> {error, Rest}; % error (custom)
+phpval([$b,$:,$1,$; | Rest]) -> {true, Rest}; % true
+phpval([$b,$:,$0,$; | Rest]) -> {false, Rest}; % false
+%% r seems to be a recursive reference to something, represented as an int.
+phpval([$r, $: | Rest]) ->
+ {RefNum, [$; | Rest1]} = string:to_integer(Rest),
+ {{php_ref, RefNum}, Rest1};
+%% int
+phpval([$i, $: | Rest])->
+ {Num, [$; | Rest1]} = string:to_integer(Rest),
+ {Num, Rest1};
+%% double / float
+%% NB: php floats can be ints, and string:to_float doesn't like that.
+phpval([$d, $: | Rest]) ->
+ {Num, [$; | Rest1]} = case string:to_float(Rest) of
+ {error, no_float} -> string:to_integer(Rest);
+ {N,R} -> {N,R}
+ end,
+ {Num, Rest1};
+%% string
+phpval([$s, $: | Rest]) ->
+ {Len, [$: | Rest1]} =string:to_integer(Rest),
+ S = list_to_binary(string:sub_string(Rest1, 2, Len+1)),
+ {S, lists:nthtail(Len+3, Rest1)};
+%% array
+phpval([$a, $: | Rest]) ->
+ {NumEntries, [$:, 123 | Rest1]} =string:to_integer(Rest), % 123 is ${
+ {Array, Rest2} = takeval(Rest1, NumEntries*2),
+ {arraytidy(Array), Rest2};
+
+%% object O:4:\"User\":53:{
+phpval([$O, $: | Rest]) ->
+ {ClassnameLen, [$: | Rest1]} =string:to_integer(Rest),
+ %% Rest1: "classname":NumEnt:{..
+ Classname = string:sub_string(Rest1, 2, ClassnameLen+1),
+ Rest1b = lists:nthtail(ClassnameLen+3, Rest1),
+ {NumEntries, [$:, 123 | Rest2]} = string:to_integer(Rest1b), % 123 is ${
+ {Classvals, Rest3} = takeval(Rest2, NumEntries*2),
+ {{class, Classname, arraytidy(Classvals)}, Rest3};
+
+%% string does not begin with serialized data
+phpval(String) ->
+ {none, String}.
+
+%%
+%% Helpers:
+%%
+%% convert [ k1,v1,k2,v2,k3,v3 ] into [ {k1,v2}, {k2,v2}, {k3,v3} ]
+arraytidy(L) ->
+ lists:reverse(lists:foldl(fun arraytidy/2, [], L)).
+
+arraytidy(El, [{K, '_'} | L]) ->
+ [{listify(K), El} | L];
+arraytidy(El, L) ->
+ [{El, '_'} | L].
+
+%%% Make binary array keys into lists
+listify(K) when is_binary(K) ->
+ listify(binary_to_list(K));
+listify(K) ->
+ K.
+
+get_opt(Opt, Opts, Default) ->
+ case lists:keysearch(Opt, 1, Opts) of
+ {value, {Opt, Value}} ->
+ Value;
+ _ -> Default
+ end.
Please sign in to comment.
Something went wrong with that request. Please try again.