1%%% File    : yaws_sendfile.erl
2%%% Author  : Steve Vinoski <vinoski@ieee.org>
3%%% Description : interface to sendfile linked-in driver for Yaws
4%%% Created :  9 Nov 2008 by Steve Vinoski <vinoski@ieee.org>
5
6-module(yaws_sendfile).
7-author('vinoski@ieee.org').
8
9-include("../include/yaws.hrl").
10-include_lib("kernel/include/file.hrl").
11
12-export([send/2, send/3, send/4]).
13-export([have_sendfile/0, have_erlang_sendfile/0, check_gc_flags/1]).
14
15%% export bytes_to_transfer to avoid warning when sendfile is disabled (or not
16%% supported)
17-export([bytes_to_transfer/3]).
18
19
20-ifdef(HAVE_SENDFILE).
21
22-behavior(gen_server).
23-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
24         code_change/3]).
25
26-export([start_link/0, stop/0]).
27
28have_sendfile() -> true.
29
30-else.
31
32have_sendfile() -> false.
33
34-endif.
35
36have_erlang_sendfile() -> yaws_dynopts:have_erlang_sendfile().
37
38check_gc_flags(GC) ->
39    %% below, ignore dialyzer warning:
40    %% The pattern depends on the macro HAVE_ERLANG_SENDFILE
41    case have_erlang_sendfile() of
42        false when ?gc_use_erlang_sendfile(GC) ->
43            error_logger:error_msg("Cannot use file:sendfile/5: not supported, "
44                                   "gen_tcp:send/2 will be used instead.~n",
45                                   []);
46        _ ->
47            ok
48    end,
49
50    %% below, ignore dialyzer warning:
51    %% The pattern depends on the macro HAVE_SENDFILE
52    case have_sendfile() of
53        false when ?gc_use_yaws_sendfile(GC) ->
54            error_logger:error_msg("Cannot use Yaws sendfile linked-in driver:"
55                                   " not supported, gen_tcp:send/2 will be used"
56                                   " instead.~n",
57                                   []);
58        _ ->
59            ok
60    end.
61
62
63send(Out, Filename) ->
64    send(Out, Filename, 0, all).
65send(Out, Filename, Offset) ->
66    send(Out, Filename, Offset, all).
67
68send(Out, Filename, Offset, Count) ->
69    GC             = get(gc),
70    ChunkSize      = GC#gconf.large_file_chunk_size,
71    ErlangSendFile = ?gc_use_erlang_sendfile(GC),
72    YawsSendFile   = ?gc_use_yaws_sendfile(GC),
73    if
74        ErlangSendFile ->
75            erlang_sendfile(Out, Filename, Offset, Count, ChunkSize);
76        YawsSendFile ->
77            yaws_sendfile(Out, Filename, Offset, Count, ChunkSize);
78        true ->
79            compat_send(Out, Filename, Offset, Count, ChunkSize)
80    end.
81
82
83bytes_to_transfer(Filename, Offset, Count) ->
84    case Count of
85        all ->
86            case file:read_file_info(Filename) of
87                {ok, #file_info{size = Size}} -> Size - Offset;
88                Error -> Error
89            end;
90        Count when is_integer(Count) ->
91            Count;
92        _ ->
93            {error, badarg}
94    end.
95
96
97erlang_sendfile(Out, Filename, Offset, Count, ChunkSize) ->
98    case have_erlang_sendfile() of
99        true ->
100            Count1 = bytes_to_transfer(Filename, Offset, Count),
101            case Count1 of
102                {error, _}=Error1 ->
103                    Error1;
104                _ ->
105                    case file:open(Filename, [raw, read, binary]) of
106                        {ok, RawFile} ->
107                            Res = file:sendfile(RawFile, Out, Offset, Count1,
108                                                [{chunk_size, ChunkSize}]),
109                            ok = file:close(RawFile),
110                            Res;
111                        Error2 ->
112                            Error2
113                    end
114            end;
115        false ->
116            compat_send(Out, Filename, Offset, Count, ChunkSize)
117     end.
118
119
120-ifdef(HAVE_SENDFILE).
121
122yaws_sendfile(Out, Filename, Offset, Count, ChunkSize) ->
123    Count1 = bytes_to_transfer(Filename, Offset, Count),
124    case Count1 of
125        {error, _}=Error ->
126            Error;
127        _ ->
128            case prim_inet:getfd(Out) of
129                {ok, SocketFd} ->
130                    do_send(Out, SocketFd, Filename, Offset, Count1, ChunkSize);
131                Error2 ->
132                    Error2
133            end
134    end.
135
136
137-record(state, {
138          port,                    % driver port
139          caller_tbl               % table mapping socket fd to caller
140         }).
141
142start_link() ->
143    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
144
145stop() ->
146    gen_server:cast(?MODULE, stop).
147
148init([]) ->
149    process_flag(trap_exit, true),
150    Shlib = "yaws_sendfile_drv",
151    Dir   = filename:join(yaws:get_priv_dir(), "lib"),
152    case erl_ddll:load_driver(Dir, Shlib) of
153        ok -> ok;
154        {error, already_loaded} -> ok;
155        {error, Reason} ->
156	    exit({error,
157		  "could not load driver " ++ Shlib ++ ": " ++
158		  erl_ddll:format_error(Reason)})
159    end,
160    Port = open_port({spawn, Shlib}, [binary]),
161    CallerTable = ets:new(yaws_sendfile, []),
162    {ok, #state{port = Port, caller_tbl = CallerTable}}.
163
164handle_call({send, SocketFd, Msg}, From, State) ->
165    true = erlang:port_command(State#state.port, Msg),
166    true = ets:insert(State#state.caller_tbl, {SocketFd, From}),
167    {noreply, State};
168handle_call(_Req, _From, State) ->
169    {reply, ok, State}.
170
171handle_info({_, {data, <<Cnt:64, SocketFd:32, Res:8, Err/binary>>}}, State) ->
172    Reply = case Res of
173                1 ->
174                    {ok, Cnt};
175                0 ->
176                    {error,
177                     list_to_atom(
178                       lists:takewhile(fun(El) -> El =/= 0 end,
179                                       binary_to_list(Err)))}
180            end,
181    CallerTable = State#state.caller_tbl,
182    [{SocketFd, From}] = ets:lookup(CallerTable, SocketFd),
183    gen_server:reply(From, Reply),
184    ets:delete(CallerTable, SocketFd),
185    {noreply, State};
186handle_info(_Info, State) ->
187    {noreply, State}.
188
189handle_cast(stop, State) ->
190    {stop, State};
191handle_cast(_, State) ->
192    {noreply, State}.
193
194terminate(_Reason, #state{port = Port, caller_tbl = CallerTable}) ->
195    erlang:port_close(Port),
196    receive {'EXIT', Port, _Reason} -> ok
197    after 0 -> ok
198    end,
199    ets:delete(CallerTable),
200    ok.
201
202code_change(_OldVsn, Data, _Extra) ->
203    {ok, Data}.
204
205do_send(_Out, _SocketFd, _Filename, _Offset, Count, _) when Count =< 0 ->
206    {ok, 0};
207do_send(Out, SocketFd, Filename0, Offset, Count, ChunkSize) ->
208    Filename = case file:native_name_encoding() of
209                   latin1 -> Filename0;
210                   utf8 -> unicode:characters_to_binary(Filename0)
211               end,
212    Call = list_to_binary([<<Offset:64, Count:64, SocketFd:32>>,
213                           Filename, <<0:8>>]),
214    case gen_server:call(?MODULE, {send, SocketFd, Call}, infinity) of
215        {error, eoverflow} ->
216            compat_send(Out, Filename, Offset, Count, ChunkSize);
217        Else ->
218            Else
219    end.
220
221-else.
222
223yaws_sendfile(Out, Filename, Offset, Count, ChunkSize) ->
224    compat_send(Out, Filename, Offset, Count, ChunkSize).
225
226-endif.
227
228
229
230compat_send(Out, Filename, Offset, Count0, ChunkSize) ->
231    Count = case Count0 of
232                0 -> all;
233                _ -> Count0
234            end,
235    case file:open(Filename, [read, binary, raw]) of
236        {ok, Fd} ->
237            file:position(Fd, {bof, Offset}),
238            Ret = loop_send(Fd, ChunkSize, file:read(Fd, ChunkSize), Out,
239                            Count, 0),
240            file:close(Fd),
241            Ret;
242        Err ->
243            Err
244    end.
245
246loop_send(Fd, ChunkSize, {ok, Bin}, Out, all, BytesSent) ->
247    case gen_tcp:send(Out, Bin) of
248        ok ->
249            loop_send(Fd, ChunkSize, file:read(Fd, ChunkSize), Out, all,
250                      BytesSent+size(Bin));
251        Err ->
252            Err
253    end;
254loop_send(_Fd, _ChunkSize, eof, _Out, _, BytesSent) ->
255    {ok, BytesSent};
256loop_send(Fd, ChunkSize, {ok, Bin}, Out, Count, BytesSent) ->
257    Sz = size(Bin),
258    if Sz < Count ->
259            case gen_tcp:send(Out, Bin) of
260                ok ->
261                    loop_send(Fd, ChunkSize, file:read(Fd, ChunkSize),
262                              Out, Count-Sz, BytesSent+Sz);
263                Err ->
264                    Err
265            end;
266       Sz == Count ->
267            case gen_tcp:send(Out, Bin) of
268                ok  -> {ok, BytesSent+Sz};
269                Err -> Err
270            end;
271       Sz > Count ->
272            <<Deliver:Count/binary , _/binary>> = Bin,
273            case gen_tcp:send(Out, Deliver) of
274                ok  -> {ok, BytesSent+Count};
275                Err -> Err
276            end
277    end;
278loop_send(_Fd, _, Err, _, _, _) ->
279    Err.
280