src/perc.erl

%%% @copyright 2012-2021 Michael Santos <michael.santos@gmail.com>

%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions
%%% are met:
%%%
%%% 1. Redistributions of source code must retain the above copyright notice,
%%% this list of conditions and the following disclaimer.
%%%
%%% 2. Redistributions in binary form must reproduce the above copyright
%%% notice, this list of conditions and the following disclaimer in the
%%% documentation and/or other materials provided with the distribution.
%%%
%%% 3. Neither the name of the copyright holder nor the names of its
%%% contributors may be used to endorse or promote products derived from
%%% this software without specific prior written permission.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
%%% "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
%%% LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
%%% A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
%%% HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
%%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
%%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
%%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
%%% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
%%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-module(perc).

-export([
    kill/2,

    getuid/0,
    geteuid/0,
    getgid/0,
    getegid/0,

    getgroups/0,
    setgroups/1,

    setresuid/3,
    setresgid/3,

    renice/2,
    getpriority/2,
    setpriority/3,

    umask/0, umask/1,
    getumask/0,

    status/0,

    getrlimit/1,
    setrlimit/2,

    % Linux only
    prctl/3,
    prlimit/4,
    signalfd/1, signalfd/2,
    sigaddset/1,
    close/1
]).

-export_type([
    int32_t/0,
    pid_t/0,
    posix/0,
    priority/0,
    uint32_t/0,
    uint64_t/0
]).

-type posix() ::
    'e2big'
    | 'eacces'
    | 'eaddrinuse'
    | 'eaddrnotavail'
    | 'eadv'
    | 'eafnosupport'
    | 'eagain'
    | 'ealign'
    | 'ealready'
    | 'ebade'
    | 'ebadf'
    | 'ebadfd'
    | 'ebadmsg'
    | 'ebadr'
    | 'ebadrpc'
    | 'ebadrqc'
    | 'ebadslt'
    | 'ebfont'
    | 'ebusy'
    | 'ecapmode'
    | 'echild'
    | 'echrng'
    | 'ecomm'
    | 'econnaborted'
    | 'econnrefused'
    | 'econnreset'
    | 'edeadlk'
    | 'edeadlock'
    | 'edestaddrreq'
    | 'edirty'
    | 'edom'
    | 'edotdot'
    | 'edquot'
    | 'eduppkg'
    | 'eexist'
    | 'efault'
    | 'efbig'
    | 'ehostdown'
    | 'ehostunreach'
    | 'eidrm'
    | 'einit'
    | 'einprogress'
    | 'eintr'
    | 'einval'
    | 'eio'
    | 'eisconn'
    | 'eisdir'
    | 'eisnam'
    | 'el2hlt'
    | 'el2nsync'
    | 'el3hlt'
    | 'el3rst'
    | 'elbin'
    | 'elibacc'
    | 'elibbad'
    | 'elibexec'
    | 'elibmax'
    | 'elibscn'
    | 'elnrng'
    | 'eloop'
    | 'emfile'
    | 'emlink'
    | 'emsgsize'
    | 'emultihop'
    | 'enametoolong'
    | 'enavail'
    | 'enet'
    | 'enetdown'
    | 'enetreset'
    | 'enetunreach'
    | 'enfile'
    | 'enoano'
    | 'enobufs'
    | 'enocsi'
    | 'enodata'
    | 'enodev'
    | 'enoent'
    | 'enoexec'
    | 'enolck'
    | 'enolink'
    | 'enomem'
    | 'enomsg'
    | 'enonet'
    | 'enopkg'
    | 'enoprotoopt'
    | 'enospc'
    | 'enosr'
    | 'enostr'
    | 'enosym'
    | 'enosys'
    | 'enotblk'
    | 'enotcapable'
    | 'enotconn'
    | 'enotdir'
    | 'enotempty'
    | 'enotnam'
    | 'enotrecoverable'
    | 'enotsock'
    | 'enotsup'
    | 'enotty'
    | 'enotuniq'
    | 'enxio'
    | 'eopnotsupp'
    | 'eoverflow'
    | 'eownerdead'
    | 'eperm'
    | 'epfnosupport'
    | 'epipe'
    | 'eproclim'
    | 'eprocunavail'
    | 'eprogmismatch'
    | 'eprogunavail'
    | 'eproto'
    | 'eprotonosupport'
    | 'eprototype'
    | 'erange'
    | 'erefused'
    | 'eremchg'
    | 'eremdev'
    | 'eremote'
    | 'eremoteio'
    | 'eremoterelease'
    | 'erofs'
    | 'erpcmismatch'
    | 'erremote'
    | 'eshutdown'
    | 'esocktnosupport'
    | 'espipe'
    | 'esrch'
    | 'esrmnt'
    | 'estale'
    | 'esuccess'
    | 'etime'
    | 'etimedout'
    | 'etoomanyrefs'
    | 'etxtbsy'
    | 'euclean'
    | 'eunatch'
    | 'eusers'
    | 'eversion'
    | 'ewouldblock'
    | 'exdev'
    | 'exfull'.

-type int32_t() :: -16#7fffffff..16#7fffffff.
-type uint32_t() :: 0..16#ffffffff.
-type uint64_t() :: 0..16#ffffffffffffffff.
-type pid_t() :: int32_t().

-on_load(on_load/0).

on_load() ->
    erlang:load_nif(progname(), []).

%% @doc kill(2): send a signal to a Unix process
%%
%% Process IDs and signals are signed 32-bit integers. A signal
%% can also be the lower cased signal name as an atom.
%%
%% Sending a signal to PID 0 will send the signal to the Erlang VM.
-spec kill(pid_t(), atom() | integer()) -> ok | {error, posix()}.
kill(Pid, Signal) when is_integer(Signal) ->
    kill_nif(Pid, Signal);
kill(Pid, Signal) when is_atom(Signal) ->
    kill_nif(Pid, perc_signal:define(Signal)).

kill_nif(_, _) ->
    erlang:nif_error(not_implemented).

%% @doc getpriority(2): get the process priority
%%
%% Get the priority (the "nice" value) of processes by pid, process
%% group or user.
-spec getpriority(int32_t(), int32_t()) -> {ok, int32_t()} | {error, posix()}.
getpriority(_, _) ->
    erlang:nif_error(not_implemented).

%% @doc setpriority(2): set the process priority
%%
%% Set the priority (the "nice" value) of processes by pid, process
%% group or user.
-spec setpriority(int32_t(), int32_t(), int32_t()) -> ok | {error, posix()}.
setpriority(_, _, _) ->
    erlang:nif_error(not_implemented).

%% @doc getuid(2): get user identity
-spec getuid() -> uint32_t().
getuid() ->
    erlang:nif_error(not_implemented).

%% @doc geteuid(2): get effective user id
-spec geteuid() -> uint32_t().
geteuid() ->
    erlang:nif_error(not_implemented).

%% @doc getgid(2): get group identity
-spec getgid() -> uint32_t().
getgid() ->
    erlang:nif_error(not_implemented).

%% @doc getegid(2): get effective group id
-spec getegid() -> uint32_t().
getegid() ->
    erlang:nif_error(not_implemented).

%% @doc getgroups(2): retrieve the list of supplementary groups
-spec getgroups() -> [uint32_t()].
getgroups() ->
    erlang:nif_error(not_implemented).

%% @doc setgroups(2): set the list of supplementary groups
-spec setgroups([uint32_t()]) -> ok | {error, posix()}.
setgroups(_) ->
    erlang:nif_error(not_implemented).

%% @doc setresuid(2): set real, effective and saved user id
-spec setresuid(uint32_t(), uint32_t(), uint32_t()) -> ok | {error, posix()}.
setresuid(_, _, _) ->
    erlang:nif_error(not_implemented).

%% @doc setresgid(2): set real, effective and saved group id
-spec setresgid(uint32_t(), uint32_t(), uint32_t()) -> ok | {error, posix()}.
setresgid(_, _, _) ->
    erlang:nif_error(not_implemented).

%% @doc renice: reset the process priority
%%
%% Convenience wrapper around getpriority/2 and setpriority/3,
%% similar to renice(1).
%%
%% WARNING: renice/2 makes successive calls to getpriority/2 and
%% setpriority/3. Since this sequence is not atomic, the priority
%% may change between calls or the process may have been terminated.
%%
%% Sets the priority of a process or processes by pid, pgroup or
%% user. The new priority may be an integer or a list containing a
%% relative priority, indicated by using a "+" or "-". For example,
%% using "+10" will increase the niceness of the process by 10.
-spec renice(priority(), string() | int32_t()) -> ok | {error, posix()}.
renice(Type, Priority) ->
    {Which, Who} = prio(Type),
    renice(Which, Who, Priority).

-spec renice(int32_t(), int32_t(), string() | int32_t()) -> ok | {error, posix()}.
renice(Which, Who, "+" ++ Priority) ->
    case getpriority(Which, Who) of
        {ok, OldPrio} ->
            N = list_to_integer(Priority),
            setpriority(Which, Who, OldPrio + N);
        Error ->
            Error
    end;
renice(Which, Who, "-" ++ Priority) ->
    case getpriority(Which, Who) of
        {ok, OldPrio} ->
            N = list_to_integer(Priority),
            setpriority(Which, Who, OldPrio - N);
        Error ->
            Error
    end;
renice(Which, Who, Priority) when is_list(Priority) ->
    setpriority(Which, Who, list_to_integer(Priority));
renice(Which, Who, Priority) when is_integer(Priority) ->
    setpriority(Which, Who, Priority).

%% @doc status: retrieve process status
%%
%% Reads /proc/self/status as a map.
-spec status() -> {ok, #{binary() => binary()}} | {error, atom()}.
status() ->
    case file:read_file("/proc/self/status") of
        {error, _} = Error ->
            Error;
        {ok, Status} ->
            {ok,
                maps:from_list(
                    [
                        list_to_tuple(binary:split(N, <<":\t">>))
                        || N <- binary:split(Status, <<"\n">>, [global, trim]),
                           N /= <<>>
                    ]
                )}
    end.

%% @doc getumask(3): safer version of umask/0
%%
%% Obtain the current process mask by parsing /proc/self/status,
%% avoiding the race condition in umask/0.
%%
%% If /proc/self/status does not exist or is not parsable, getumask/0
%% fails back to umask/0.
-spec getumask() -> int32_t().
getumask() ->
    case status() of
        {error, _} ->
            umask();
        {ok, Status} ->
            case maps:find(<<"Umask">>, Status) of
                error ->
                    umask();
                {ok, Mask} ->
                    binary_to_integer(Mask, 8)
            end
    end.

%% @doc umask(2): get the process mask
%%
%% WARNING: umask/0 is destructive: the umask is retrieved by setting
%% the process mask to 0, then re-setting it back to the original
%% mask. Between the successive calls to umask(2), the process
%% mask is 0. An erlang process calling umask/0 concurrently with
%% a process creating a file may have unexpected results.
-spec umask() -> int32_t().
umask() ->
    umask_nif().

umask_nif() ->
    erlang:nif_error(not_implemented).

%% @doc umask(2): set the process mask
%%
%% Sets the file creation mask for beam. The mask may be either
%% an integer or a list representing an octal number, e.g., either
%% 8#022 or "022".
%%
%% The old mask value is returned. To retrieve the current umask,
%% use umask/0 or getumask/0.
-spec umask(string() | int32_t()) -> int32_t().
umask(Mask) when is_list(Mask) ->
    umask(list_to_integer(Mask, 8));
umask(Mask) when is_integer(Mask) ->
    umask_nif(Mask).

umask_nif(_) ->
    erlang:nif_error(not_implemented).

%% @doc getrlimit(2): get process resource limits
%%
%% Get process limits for beam. See prlimit/4 for a list of the
%% resource atoms.
%%
%% The value returned is a struct rlimit:
%%
%% ```
%% 1> {ok, <<Soft:8/native-unsigned-integer-unit:8, Hard:8/native-unsigned-integer-unit:8>>} = perc:getrlimit(rlimit_nofile).
%% 2> Soft.
%% 1024
%% 3> Hard.
%% 4096
%% '''
-spec getrlimit(atom() | int32_t()) -> {ok, binary()} | {error, posix()}.
getrlimit(Resource) when is_atom(Resource) ->
    getrlimit(perc_rlimit:define(Resource));
getrlimit(Resource) ->
    % struct rlimit {
    %   rlim_t rlim_cur;  /* Soft limit */
    %   rlim_t rlim_max;  /* Hard limit (ceiling for rlim_cur) */
    % };
    Size = erlang:system_info({wordsize, external}),
    getrlimit_nif(Resource, <<0:(16 * Size)>>).

getrlimit_nif(_, _) ->
    erlang:nif_error(not_implemented).

%% @doc setrlimit(2): set process resource limits
%%
%% Set process limits for beam. See prlimit/4 for a list of the
%% resource atoms.
-spec setrlimit(atom() | int32_t(), atom() | int32_t()) -> {ok, binary()} | {error, posix()}.
setrlimit(Resource, Limit) when is_atom(Resource) ->
    setrlimit(perc_rlimit:define(Resource), Limit);
setrlimit(Resource, Limit) ->
    setrlimit_nif(Resource, Limit).

setrlimit_nif(_, _) ->
    erlang:nif_error(not_implemented).

-spec prctl(int32_t(), uint32_t() | uint64_t(), binary()) -> ok | {error, posix()}.
prctl(_, _, _) ->
    erlang:nif_error(not_implemented).

%% @doc prlimit(2): set resource limits for a specified process
%%
%% Linux only: on other platforms, {error, unsupported} will be
%% returned to the caller.
%%
%% Set or retrieve process limits for a process. Passing in an
%% empty binary for NewLimit or OldLimit indicates the caller is
%% not interested in these values.
%%
%% The binary size of NewLimit/OldLimit must otherwise match the size
%% of a struct rlimit for the platform. struct rlimit is usually
%% composed of two 8 byte values in native format. To retrieve the
%% current settings, pass in a zeroed 16 byte value.
-spec prlimit(pid_t(), atom() | int32_t(), binary(), binary()) ->
    {ok, binary(), binary()} | {error, posix()}.
prlimit(Pid, Resource, New, Old) when is_atom(Resource) ->
    prlimit(Pid, perc_rlimit:define(Resource), New, Old);
prlimit(Pid, Resource, New, Old) ->
    prlimit_nif(Pid, Resource, New, Old).

prlimit_nif(_, _, _, _) ->
    erlang:nif_error(not_implemented).

-spec sigaddset([atom() | int32_t()]) -> {ok, binary()} | {error, posix()}.
sigaddset(Signals) when is_list(Signals) ->
    Signals1 = lists:map(
        fun
            (N) when is_integer(N) ->
                N;
            (N) when is_atom(N) ->
                perc_signal:define(N)
        end,
        Signals
    ),
    sigaddset_nif(Signals1).

sigaddset_nif(_) ->
    erlang:nif_error(not_implemented).

-spec close(int32_t()) -> ok | {error, posix()}.
close(_) ->
    erlang:nif_error(not_implemented).

-spec signalfd(binary()) -> {ok, int32_t()} | {error, posix()}.
signalfd(Mask) ->
    signalfd(-1, Mask).

-spec signalfd(int32_t(), binary()) -> {ok, int32_t()} | {error, posix()}.
signalfd(Fd, Mask) ->
    signalfd_nif(Fd, Mask).

signalfd_nif(_, _) ->
    erlang:nif_error(not_implemented).

-type priority() ::
    int32_t()
    | {pid, int32_t()}
    | {pgrp, int32_t()}
    | {user, int32_t()}.

-spec prio(priority()) -> {int32_t(), int32_t()}.
prio({pid, N}) ->
    {perc_prio:define(prio_process), N};
prio({pgrp, N}) ->
    {perc_prio:define(prio_pgrp), N};
prio({user, N}) ->
    {perc_prio:define(prio_user), N};
prio(N) when is_integer(N) ->
    {perc_prio:define(prio_process), N}.

-spec progname() -> binary() | string().
progname() ->
    case code:priv_dir(?MODULE) of
        {error, bad_name} ->
            filename:join([
                filename:dirname(code:which(?MODULE)),
                "..",
                "priv",
                ?MODULE
            ]);
        Dir ->
            filename:join([Dir, ?MODULE])
    end.