Skip to main content

src/fcgi_ffi.erl

-module(fcgi_ffi).

-include_lib("kernel/include/file.hrl").

-export([open_and_size/1, sendfile/4, listen_tcp/2, listen_unix/1, close_socket/1, send/2,
         controlling_process/2, recv/3, delete_path/1, close_file/1, socket_port/1]).

-define(LISTEN_OPTS, [binary, {active, false}, {packet, raw}, {backlog, 1024}]).

open_and_size(Path) ->
    case file:open(Path, [read, raw, binary]) of
        {ok, IoDevice} ->
            size_and_rewind(IoDevice, Path);
        {error, Reason} ->
            {error, translate_error(Path, Reason)}
    end.

size_and_rewind(IoDevice, Path) ->
    case file:position(IoDevice, eof) of
        {ok, Size} ->
            rewind_with_size(IoDevice, Path, Size);
        {error, Reason} ->
            close_and_translate(IoDevice, Path, Reason)
    end.

rewind_with_size(IoDevice, Path, Size) ->
    case file:position(IoDevice, 0) of
        {ok, 0} ->
            {ok, {IoDevice, Size}};
        {error, Reason} ->
            close_and_translate(IoDevice, Path, Reason)
    end.

close_and_translate(IoDevice, Path, Reason) ->
    _ = file:close(IoDevice),
    {error, translate_error(Path, Reason)}.

sendfile(IoDevice, Socket, Offset, Bytes) ->
    case file:sendfile(IoDevice, Socket, Offset, Bytes, []) of
        {ok, Sent} ->
            {ok, Sent};
        {error, _} ->
            {error, nil}
    end.

listen_tcp(Host, Port) ->
    case inet:parse_address(binary_to_list(Host)) of
        {ok, Ip} ->
            Family =
                case tuple_size(Ip) of
                    4 ->
                        inet;
                    8 ->
                        inet6
                end,
            map_posix(gen_tcp:listen(Port, [Family, {ip, Ip} | ?LISTEN_OPTS]));
        {error, _} ->
            {error, {invalid_host, Host}}
    end.

listen_unix(PathBin) ->
    case file:read_file_info(PathBin) of
        {ok, #file_info{type = other}} ->
            case is_stale_socket(PathBin) of
                true ->
                    _ = file:delete(PathBin),
                    map_posix(gen_tcp:listen(0, [{ifaddr, {local, PathBin}} | ?LISTEN_OPTS]));
                false ->
                    {error, {path_exists, PathBin}}
            end;
        {ok, _} ->
            {error, {path_exists, PathBin}};
        {error, enoent} ->
            map_posix(gen_tcp:listen(0, [{ifaddr, {local, PathBin}} | ?LISTEN_OPTS]));
        {error, Reason} ->
            {error, {posix, Reason}}
    end.

is_stale_socket(PathBin) ->
    case gen_tcp:connect({local, PathBin}, 0, [binary, {active, false}], 100) of
        {ok, Sock} ->
            _ = gen_tcp:close(Sock),
            false;
        {error, econnrefused} ->
            true;
        {error, _} ->
            false
    end.

close_socket(Socket) ->
    _ = gen_tcp:close(Socket),
    nil.

socket_port(Socket) ->
    case inet:port(Socket) of
        {ok, Port} ->
            {ok, Port};
        {error, Reason} ->
            {error, {posix, Reason}}
    end.

send(Socket, Data) ->
    case gen_tcp:send(Socket, Data) of
        ok ->
            {ok, nil};
        {error, _} ->
            {error, nil}
    end.

controlling_process(Socket, Pid) ->
    map_posix(gen_tcp:controlling_process(Socket, Pid)).

recv(Socket, Size, TimeoutMs) ->
    map_posix(gen_tcp:recv(Socket, Size, TimeoutMs)).

delete_path(PathBin) ->
    _ = file:delete(PathBin),
    nil.

close_file(Handle) ->
    _ = file:close(Handle),
    nil.

map_posix(ok) ->
    {ok, nil};
map_posix({ok, _} = Reply) ->
    Reply;
map_posix({error, Reason}) ->
    {error, {posix, Reason}}.

translate_error(Path, enoent) ->
    {file_not_found, Path};
translate_error(Path, eacces) ->
    {file_access_denied, Path};
translate_error(Path, eperm) ->
    {file_access_denied, Path};
translate_error(Path, eisdir) ->
    {file_is_directory, Path};
translate_error(Path, Reason) ->
    {file_other, Path, list_to_binary(io_lib:format("~p", [Reason]))}.