1%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.
2%%%
3%%% Redistribution and use in source and binary forms, with or without
4%%% modification, are permitted provided that the following conditions are met:
5%%%
6%%%   1. Redistributions of source code must retain the above copyright notice,
7%%%      this list of conditions and the following disclaimer.
8%%%   2. Redistributions in binary form must reproduce the above copyright
9%%%      notice, this list of conditions and the following disclaimer in the
10%%%      documentation and/or other materials provided with the distribution.
11%%%
12%%% THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR
13%%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
14%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
15%%% EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
16%%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
19%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
23%% @doc erlang wrapper for syslog port
24
25-module(syslog).
26
27-behaviour(gen_server).
28
29-define(DRV_NAME, "syslog_drv").
30
31%% this constant must match the same in syslog_drv.c
32-define(SYSLOGDRV_OPEN,  1).
33
34%% API
35-export([
36         start/0,
37         start_link/0,
38         stop/0,
39         open/3,
40         log/3,
41         log/4,
42         close/1,
43         priority/1,
44         facility/1,
45         openlog_opt/1,
46         openlog_opts/1,
47         load/0,
48         unload/0
49        ]).
50
51%% gen_server callbacks
52-export([
53         init/1,
54         handle_call/3,
55         handle_cast/2,
56         handle_info/2,
57         terminate/2,
58         code_change/3
59        ]).
60
61-ifdef(TEST).
62-include_lib("eunit/include/eunit.hrl").
63-endif.
64
65-record(state, {}).
66
67-type priority() :: emerg | alert | crit | err |
68                    warning | notice | info | debug | non_neg_integer().
69-type facility() :: kern | user | mail | daemon | auth | syslog |
70                    lpr | news | uucp | cron | authpriv | ftp |
71                    netinfo | remoteauth | install | ras |
72                    local0 | local1 | local2 | local3 |
73                    local4 | local5 | local6 | local7 | non_neg_integer().
74-type openlog_opt() :: pid | cons | odelay | ndelay | perror | pos_integer().
75-export_type([priority/0, facility/0, openlog_opt/0]).
76
77%%% API %%%
78
79-spec start() ->
80    {ok, pid()} | ignore | {error, any()}.
81
82start() ->
83    gen_server:start({local, ?MODULE}, ?MODULE, [], []).
84
85-spec start_link() ->
86    {ok, pid()} | ignore | {error, any()}.
87
88start_link() ->
89    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
90
91-spec stop() ->
92    ok.
93
94stop() ->
95    gen_server:cast(?MODULE, stop).
96
97-spec open(Ident :: string(),
98           Logopt :: list(openlog_opt()),
99           Facility :: facility()) ->
100    {ok, port()} |
101    {error, any()}.
102
103open(Ident, Logopt, Facility) ->
104    Log = erlang:open_port({spawn, ?DRV_NAME}, [binary]),
105    Args = term_to_binary({Ident, openlog_opts(Logopt), facility(Facility)}),
106    try erlang:port_control(Log, ?SYSLOGDRV_OPEN, Args) of
107        <<>> ->
108            {ok, Log};
109        BinError ->
110            binary_to_term(BinError)
111    catch
112        _:Reason ->
113            {error, Reason}
114    end.
115
116-spec log(Log :: port(),
117          Priority :: priority(),
118          Message :: iolist()) ->
119    ok.
120
121log(_Log, _Priority, []) ->
122    ok;
123log(Log, Priority, Message) ->
124    NumPri = priority(Priority),
125    %% encode the priority value as a 4-byte integer in network order, and
126    %% add a 0 byte to the end of the command data to act as a NUL character
127    true = erlang:port_command(Log, [<<NumPri:32/big>>, Message, <<0:8>>]),
128    ok.
129
130-spec log(Log :: port(),
131          Priority :: priority(),
132          FormatStr :: string(),
133          FormatArgs :: list()) ->
134    ok.
135
136log(Log, Priority, FormatStr, FormatArgs) ->
137    log(Log, Priority, io_lib:format(FormatStr, FormatArgs)).
138
139-spec close(Log :: port()) ->
140    ok.
141
142close(Log) ->
143    true = erlang:port_close(Log),
144    ok.
145
146-spec priority(N :: priority() | non_neg_integer()) ->
147    non_neg_integer().
148
149priority(emerg)     -> 0;
150priority(alert)     -> 1;
151priority(crit)      -> 2;
152priority(err)       -> 3;
153priority(warning)   -> 4;
154priority(notice)    -> 5;
155priority(info)      -> 6;
156priority(debug)     -> 7;
157priority(N) when is_integer(N), N >= 0 -> N;
158priority(_) -> erlang:error(badarg).
159
160-spec facility(N :: facility() | non_neg_integer()) ->
161    non_neg_integer().
162
163facility(kern)      -> 0;
164facility(user)      -> 8;
165facility(mail)      -> 16;
166facility(daemon)    -> 24;
167facility(auth)      -> 32;
168facility(syslog)    -> 40;
169facility(lpr)       -> 48;
170facility(news)      -> 56;
171facility(uucp)      -> 64;
172facility(cron)      -> 72;
173facility(authpriv)  -> 80;
174facility(ftp)       -> 88;
175facility(netinfo)   -> 96;
176facility(remoteauth)-> 104;
177facility(install)   -> 112;
178facility(ras)       -> 120;
179facility(local0)    -> 16 * 8;
180facility(local1)    -> 17 * 8;
181facility(local2)    -> 18 * 8;
182facility(local3)    -> 19 * 8;
183facility(local4)    -> 20 * 8;
184facility(local5)    -> 21 * 8;
185facility(local6)    -> 22 * 8;
186facility(local7)    -> 23 * 8;
187facility(N) when is_integer(N), N >= 0 -> N;
188facility(_) -> erlang:error(badarg).
189
190-spec openlog_opt(N :: openlog_opt() | pos_integer()) ->
191    pos_integer().
192
193openlog_opt(pid)    -> 1;
194openlog_opt(cons)   -> 2;
195openlog_opt(odelay) -> 4;
196openlog_opt(ndelay) -> 8;
197openlog_opt(perror) -> 20;
198openlog_opt(N) when is_integer(N), N >= 1 -> N;
199openlog_opt(_) -> erlang:error(badarg).
200
201-spec openlog_opts(N :: list(openlog_opt() | pos_integer()) |
202                        openlog_opt() | pos_integer()) ->
203    pos_integer().
204
205openlog_opts([Queue]) -> openlog_opt(Queue);
206openlog_opts([Tail|Queue]) ->
207    openlog_opt(Tail) bor openlog_opts(Queue);
208openlog_opts([]) -> 0;
209openlog_opts(N) -> openlog_opt(N).
210
211-spec load() ->
212    ok | {error, string()}.
213
214load() ->
215    PrivDir = case code:priv_dir(?MODULE) of
216                  {error, bad_name} ->
217                      EbinDir = filename:dirname(code:which(?MODULE)),
218                      AppPath = filename:dirname(EbinDir),
219                      filename:join(AppPath, "priv");
220                  Path ->
221                      Path
222              end,
223    case erl_ddll:load_driver(PrivDir, ?DRV_NAME) of
224        ok -> ok;
225        {error, already_loaded} -> ok;
226        {error, LoadError} ->
227            LoadErrorStr = erl_ddll:format_error(LoadError),
228            ErrStr = lists:flatten(
229                io_lib:format("could not load driver ~s: ~p",
230                              [?DRV_NAME, LoadErrorStr])),
231            {error, ErrStr}
232    end.
233
234-spec unload() ->
235    ok | {error, string()}.
236
237unload() ->
238    case erl_ddll:unload_driver(?DRV_NAME) of
239        ok -> ok;
240        {error, UnloadError} ->
241            UnloadErrorStr = erl_ddll:format_error(UnloadError),
242            ErrStr = lists:flatten(
243                io_lib:format("could not unload driver ~s: ~p",
244                              [?DRV_NAME, UnloadErrorStr])),
245            {error, ErrStr}
246    end.
247
248%%% gen_server callbacks %%%
249
250init([]) ->
251    case load() of
252        ok ->
253            {ok, #state{}};
254        {error, Reason} ->
255            {stop, Reason}
256    end.
257
258handle_call(_Msg, _From, State) ->
259    {reply, ok, State}.
260
261handle_cast(stop, State) ->
262    {stop, normal, State};
263handle_cast(_Msg, State) ->
264    {noreply, State}.
265
266handle_info(_Info, State) ->
267    {noreply, State}.
268
269terminate(_Reason, _State) ->
270    ok.
271
272code_change(_, State, _) ->
273    {ok, State}.
274
275%%% internal functions %%%
276
277-ifdef(TEST).
278
279openlog_opts_test() ->
280    11 = openlog_opts([1,2,8]),
281    1 = openlog_opts(pid),
282    try
283        foo = openlog_opts(foo)
284    catch
285        error:badarg ->
286            ok;
287        Reason ->
288            throw(Reason)
289    end.
290
291closed_test() ->
292    {ok, _} = syslog:start(),
293    try
294        {ok, Log} = open("test", pid, local0),
295        Self = self(),
296        {connected,Self} = erlang:port_info(Log, connected),
297        ok = close(Log),
298        try
299            close(Log)
300        catch
301            error:badarg ->
302                ok;
303            Reason1 ->
304                throw(Reason1)
305        end,
306        try
307            ok = log(Log, 8, "writing to closed log")
308        catch
309            error:badarg ->
310                ok;
311            Reason2 ->
312                throw(Reason2)
313        end
314    after
315        syslog:stop()
316    end.
317
318-endif.
319