Current section
Files
Jump to
Current section
Files
src/utils/system_utils.erl
% Copyright (C) 2010-2019 Olivier Boudeville
%
% This file is part of the Ceylan-Myriad library.
%
% This library is free software: you can redistribute it and/or modify
% it under the terms of the GNU Lesser General Public License or
% the GNU General Public License, as they are published by the Free Software
% Foundation, either version 3 of these Licenses, or (at your option)
% any later version.
% You can also redistribute it and/or modify it under the terms of the
% Mozilla Public License, version 1.1 or later.
%
% This library 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 Lesser General Public License and the GNU General Public License
% for more details.
%
% You should have received a copy of the GNU Lesser General Public
% License, of the GNU General Public License and of the Mozilla Public License
% along with this library.
% If not, see <http://www.gnu.org/licenses/> and
% <http://www.mozilla.org/MPL/>.
%
% Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com]
% Creation date: Thursday, February 11, 2010.
% Gathering of various system convenient facilities.
%
% See system_utils_test.erl for the corresponding test.
%
-module(system_utils).
% User-related functions.
-export([ get_user_name/0, get_user_name_string/0,
get_user_home_directory/0, get_user_home_directory_string/0 ]).
% Lower-level services.
-export([ await_output_completion/0, await_output_completion/1 ]).
% System-related functions.
-export([ run_executable/1, run_executable/2, run_executable/3,
run_executable/4,
get_line/1, get_line/2, get_line_helper_script/0,
get_standard_environment/0,
monitor_port/2,
evaluate_shell_expression/1, evaluate_shell_expression/2,
run_background_executable/1, run_background_executable/2,
run_background_executable/3,
evaluate_background_shell_expression/1,
evaluate_background_shell_expression/2,
get_environment_prefix/1, get_actual_expression/2,
get_environment_variable/1, set_environment_variable/2,
get_environment/0, environment_to_string/0, environment_to_string/1,
get_interpreter_version/0, get_application_version/1,
get_size_of_vm_word/0, get_size_of_vm_word_string/0,
get_size/1,
interpret_byte_size/1, interpret_byte_size_with_unit/1,
convert_byte_size_with_unit/1,
display_memory_summary/0,
get_total_physical_memory/0, get_total_physical_memory_string/0,
get_total_physical_memory_on/1, get_memory_used_by_vm/0,
get_total_memory_used/0,
get_swap_status/0, get_swap_status_string/0,
get_core_count/0, get_core_count_string/0,
get_process_count/0, get_process_count_string/0,
compute_cpu_usage_between/2, compute_cpu_usage_for/1,
compute_detailed_cpu_usage/2, get_cpu_usage_counters/0,
get_disk_usage/0, get_disk_usage_string/0,
get_mount_points/0,
get_known_pseudo_filesystems/0, get_filesystem_info/1,
filesystem_info_to_string/1,
get_current_directory_string/0,
get_operating_system_description/0,
get_operating_system_description_string/0,
get_system_description/0,
has_graphical_output/0 ]).
% Prerequisite-related section.
% Name of a (third-party) prerequisite package (ex: "ErlPort", "jsx", etc.).
-type package_name() :: string().
-export_type([ package_name/0 ]).
-export([ get_dependency_base_directory/1, get_dependency_code_directory/1,
is_json_support_available/0, get_json_unavailability_hint/0,
is_hdf5_support_available/0, get_hdf5_unavailability_hint/0 ]).
% Size, in number of bytes:
-type byte_size() :: integer().
-opaque cpu_usage_info() :: { integer(), integer(), integer(), integer(),
integer() }.
-type cpu_usage_percentages() :: { math_utils:percent(), math_utils:percent(),
math_utils:percent(), math_utils:percent(),
math_utils:percent() }.
% For record declarations and shell commands:
-include("system_utils.hrl").
% Describes the static information about a computing host:
-type host_static_info() :: #host_static_info{}.
% Describes the dynamic information about a computing host:
-type host_dynamic_info() :: #host_dynamic_info{}.
% Known real, actual types of filesystems:
-type actual_filesystem_type() :: 'ext2' | 'ext3' | 'ext4' | 'vfat'.
% Known pseudo filesystems:
-type pseudo_filesystem_type() :: 'devtmpfs' | 'tmpfs'.
% All the known types of filesystems (atom, to capture even lacking ones):
-type filesystem_type() :: actual_filesystem_type()
| pseudo_filesystem_type()
| 'unknown' | atom().
% Stores information about a filesystem:
-record( fs_info, {
% Device name (ex: /dev/sda5):
filesystem :: file_utils:path(),
% Mount point (ex: /boot):
mount_point :: file_utils:path(),
% Filesystem type (ex: 'ext4'):
type :: filesystem_type(),
% Used size, in bytes:
used_size :: byte_size(),
% Available size, in bytes:
available_size :: byte_size(),
% Number of used inodes:
used_inodes :: basic_utils:count(),
% Number of available inodes:
available_inodes :: basic_utils:count()
} ).
-type fs_info() :: #fs_info{}.
% Describes a command to be run (i.e. path to an executable, with possibly
% command-line arguments):
%
-type command() :: text_utils:ustring().
% An option used to spawn a port (others managed through specific parameters):
-type port_option() :: { 'packet', 1 | 2 | 4 }
| 'stream'
| { 'line', basic_utils:count() }.
% Return the (positive integer) return code of an executable being run
% (a.k.a. exit status):
%
% (0 means success, while a strictly positive value means error)
%
-type return_code() :: basic_utils:count().
% Output of the run of an executable:
-type command_output() :: text_utils:ustring().
% All information returned by a shell command:
-type execution_outcome() :: { return_code(), command_output() }.
% Describes a shell expression:
-type shell_expression() :: text_utils:ustring().
% Output of the evaluation of a shell expression (at least currently, only its
% standard output; no exit status):
%
-type expression_outcome() :: text_utils:ustring().
% Name of a shell environment variable:
-type env_variable_name() :: string().
% Value of a shell environment variable ('false' meaning that the corresponding
% variable is not set)
%
-type env_variable_value() :: string() | 'false'.
% Represents a shell environment (a set of variables):
-type environment() :: [ { env_variable_name(), env_variable_value() } ].
% Working directory of an executed command:
-type working_dir() :: maybe( file_utils:directory_name()
| file_utils:bin_directory_name() ).
% Basic authentication information:
-type user_name() :: string().
-type password() :: string().
-type basic_credential() :: { user_name(), password() }.
-export_type([ byte_size/0, cpu_usage_info/0, cpu_usage_percentages/0,
host_static_info/0, host_dynamic_info/0,
actual_filesystem_type/0, pseudo_filesystem_type/0,
filesystem_type/0, fs_info/0,
command/0, port_option/0, return_code/0, command_output/0,
execution_outcome/0,
shell_expression/0, expression_outcome/0,
env_variable_name/0, env_variable_value/0, environment/0,
working_dir/0,
user_name/0, password/0, basic_credential/0 ]).
% For myriad_spawn*:
-include("spawn_utils.hrl").
% User-related functions.
% Returns the name of the current user, as a plain string.
-spec get_user_name() -> string().
get_user_name() ->
case os:getenv( "USER" ) of
false ->
trace_utils:error( "The name of the user could not be "
"obtained from the shell environment "
"(no USER variable defined)." ),
throw( user_name_not_found_in_environment );
UserName ->
UserName
end.
% Returns a textual description of the name of the current user.
%
% Cannot crash.
%
-spec get_user_name_string() -> text_utils:ustring().
get_user_name_string() ->
try
io_lib:format( "user name: ~ts", [ get_user_name() ] )
catch _AnyClass:Exception ->
io_lib:format( "no user name information could be obtained (~p)",
[ Exception ] )
end.
% Returns the home directory of the current user, as a plain string.
-spec get_user_home_directory() -> string().
get_user_home_directory() ->
% Was: os:getenv( "HOME" )
case init:get_argument( home ) of
{ ok, [ [ Home ] ] } ->
Home;
Error ->
throw( { home_directory_not_found, Error } )
end.
% Returns a textual description of the home directory of the current user.
%
% Cannot crash.
%
-spec get_user_home_directory_string() -> text_utils:ustring().
get_user_home_directory_string() ->
try
io_lib:format( "user home directory: ~ts",
[ get_user_home_directory() ] )
catch _AnyClass:Exception ->
io_lib:format( "no home directory information could be "
"obtained (~p)", [ Exception ] )
end.
% Lower-level services.
% Awaits the completion of an output operation (ex: io:format/2).
%
% Especially useful when displaying an error message on the standard output and
% then immediately halting the VM, in order to avoid a race condition between
% the displaying and the halting.
%
% We use a relatively short waiting here, just out of safety. It may be in some
% cases insufficient (ex: for error traces to be sent, received and stored
% *before* the VM is halted after a throw/1 that may be executed just after).
%
% In this case, await_output_completion/1 should be used, with a larger delay.
%
-spec await_output_completion() -> void().
-ifdef(debug_mode).
% Default time-out duration (0.3 second, for loaded computers):
await_output_completion() ->
% Milliseconds:
await_output_completion( _TimeOut=300 ).
-else. % debug_mode
% Extended time-out (2.5 seconds), if for example being in production, on a
% possibly heavily loaded system:
%
% (warning: this may impact adversely the timing if intensive logging is used)
%
await_output_completion() ->
% Milliseconds:
await_output_completion( _TimeOut=2500 ).
-endif. % debug_mode
% Awaits the completion of a io:format request, with a specified time-out, in
% milliseconds.
%
% Especially useful when displaying an error message on the standard output and
% then immediately halting the VM, in order to avoid a race condition between
% the displaying and the halting.
%
-spec await_output_completion( unit_utils:millisecond() ) -> void().
await_output_completion( _TimeOut ) ->
% Not sure it is really the proper way of waiting, however should be still
% better than timer:sleep( 500 ):
%
% (we suppose that the time-out here is in milliseconds)
%trace_utils:debug( "(awaiting output completion)" ),
% Almost just a yield (re-enabled, see below):
timer:sleep( 10 ),
%trace_utils:debug( "(output completed)" ),
% Does not seem always sufficient:
% (supposing timeout() is in milliseconds)
% Does not exist anymore since Erlang 21.0
%
% (we get at runtime: {noproc,{sys,get_status,[error_logger,300]}})
%
% sys:get_status( error_logger, TimeOut ).
% We considered adding to test_facilities:start/1 a configuration of the
% default logger_std_h handler so that async_mode_qlen was set to 0 (to
% ensure synchronicity in all cases), yet we are not using
% error_logger/logger for our usual outputs (we use io:format/{1,2} rather
% than erlang:display/1, see basic_utils:display/1).
% And apparently io:format/{1,2} are actually synchronous
% (cf. http://erlang.org/pipermail/erlang-questions/2011-July/059908.html),
% so nothing seems to be done to ensure that no output can be lost.
% (time will tell, as we at least used to notice that outputs could be lost)
% As for logger, a doubt remains about its synchronicity, see
% test_facilities:start/1 about that.
%
ok.
% Functions relative to the local Erlang system.
% Section to run executables and evaluation shell expressions.
%
% The former will return both the exit code and the command output, while the
% latter will be able only to return the command output (command being then a
% full shell expression either, thus possibly reduced to just an executable with
% arguments).
% We wish we could specify a command as a single, standalone one, or as a list
% of command elements, but the lack of a string type prevents it (as the
% parameter of the called functions would be a list in both cases).
% Runs (synchronously) specified command (an executable path possibly followed
% with command-line arguments; specified as a single, standalone string), with
% no specific port option, with a standard environment, from the current working
% directory, and returns its return code (exit status) and its outputs (both the
% standard and the error ones).
%
% This function will run a specific executable, not evaluate a shell expression
% (that would possibly run executables).
%
% So one should not try to abuse this function by adding '&' at the end to
% trigger a background launch - this would just be interpreted as a last
% argument. Use run_background_executable/{1,2,3} in this module instead.
%
-spec run_executable( command() ) -> execution_outcome().
run_executable( Command ) ->
run_executable( Command, get_standard_environment() ).
% Executes (synchronously) specified command (an executable path possibly
% followed with command-line arguments; specified as a single, standalone
% string), with no specific port option, in specified shell environment and in
% the current directory, and returns its return code (exit status) and its
% outputs (both the standard and the error ones).
%
% This function will run a specific executable, not evaluate a shell expression
% (that would possibly run executables).
%
% So one should not try to abuse this function by adding '&' at the end to
% trigger a background launch - this would just be interpreted as a last
% argument. Use run_background_executable/{1,2,3} in this module instead.
%
-spec run_executable( command(), environment() ) ->
execution_outcome().
run_executable( Command, Environment ) ->
run_executable( Command, Environment, _WorkingDir=undefined ).
% Executes (synchronously) specified command (an executable path possibly
% followed with command-line arguments; specified as a single, standalone
% string), with no specific port option, in specified shell environment and
% directory, and returns its return code (exit status) and its outputs (both the
% standard and the error ones).
%
% This function will run a specific executable, not evaluate a shell expression
% (that would possibly run executables).
%
% So one should not try to abuse this function by adding '&' at the end to
% trigger a background launch - this would just be interpreted as a last
% argument. Use run_background_executable/{1,2,3} in this module instead.
%
-spec run_executable( command(), environment(),
maybe( working_dir() ) ) -> execution_outcome().
run_executable( Command, Environment, MaybeWorkingDir ) ->
% Removed: 'in'
DefaultBasePortOpts =
[ stream, exit_status, use_stdio, stderr_to_stdout, eof ],
run_executable( Command, Environment, MaybeWorkingDir,
DefaultBasePortOpts ).
% Executes (synchronously) specified executable (an executable path possibly
% followed with command-line arguments; specified as a single, standalone
% string), in specified shell environment and directory, with specified port
% options (possibly containing any relevant command-line arguments; see
% http://erlang.org/doc/man/erlang.html#open_port-2).
%
% Returns its return code (exit status) and its outputs (both the standard and
% the error ones).
%
-spec run_executable( command(), environment(), maybe( working_dir() ),
[ port_option() ] ) -> execution_outcome().
run_executable( Command, Environment, MaybeWorkingDir, PortOptions ) ->
%trace_utils:debug_fmt( "Running executable: '~s' with "
% "~s from working directory '~s', with options ~w.",
% [ Command, environment_to_string( Environment ), MaybeWorkingDir,
% PortOptions ] ),
PortOptsWithEnv = [ { env, Environment } | PortOptions ],
PortOptsWithPath = case MaybeWorkingDir of
undefined ->
PortOptsWithEnv;
WorkingDir ->
[ { cd, WorkingDir } | PortOptsWithEnv ]
end,
% Not spawn_executable, so command may include arguments:
Port = open_port( { spawn, Command }, PortOptsWithPath ),
%trace_utils:debug_fmt( "Spawned port ~p for command '~s'.",
% [ Port, Command ] ),
read_port( Port, _Data=[] ).
% Helper to read command data from a port.
read_port( Port, Data ) ->
%trace_utils:debug_fmt( "Reading port ~p (data: '~p').", [ Port, Data ] ),
receive
{ Port, { data, NewData } } ->
%trace_utils:debug_fmt( "Received data: '~p'.", [ NewData ] ),
read_port( Port, [ NewData | Data ] );
% As mentioned in the documentation, "the eof message and the
% exit_status message appear in an unspecified order":
{ Port, eof } ->
%trace_utils:debug_fmt( "Received eof (first), closing ~p.",
% [ Port ] ),
port_close( Port ),
receive
{ Port, { exit_status, ExitStatus } } ->
%trace_utils:debug_fmt(
% "Received exit_status (second): ~p.", [ ExitStatus ] ),
% Otherwise we have an enclosing list and last character is
% always "\n":
%
Output = text_utils:remove_ending_carriage_return(
lists:flatten( lists:reverse( Data ) ) ),
{ ExitStatus, Output }
end;
{ Port, { exit_status, ExitStatus } } ->
%trace_utils:debug_fmt( "Received exit_status (first): ~p.",
% [ ExitStatus ] ),
receive
{ Port, eof } ->
%trace_utils:debug_fmt( "Received eof (second), "
% "closing ~p.", [ Port ] ),
port_close( Port ),
Output = text_utils:remove_ending_carriage_return(
lists:flatten( lists:reverse( Data ) ) ),
{ ExitStatus, Output }
end;
% Added by ourselves so that we can avoid process leakage:
terminate_port ->
%trace_utils:debug_fmt( "Terminating port ~p.", [ Port ] ),
% Anyway no PID to send information to:
port_terminated
% Other messages should not be intercepted (ex: they could be in
% relation to other ongoing ports):
%
%Other ->
% trace_utils:warning_fmt( "Received unexpected message: ~p.",
% [ Other ] ),
% Other
end.
% Our version of io:get_line/1, as an external program so that the VM can be run
% with -noinput (and thus so that {text,term}_ui can be used with the same VM
% settings).
%
-spec get_line( text_utils:ustring() ) -> text_utils:ustring().
get_line( Prompt ) ->
get_line( Prompt, get_line_helper_script() ).
% Our version of io:get_line/1, as an external program so that the VM can be run
% with -noinput (and thus so that {text,term}_ui can be used with the same VM
% settings).
%
-spec get_line( text_utils:ustring(), file_utils:executable_path() ) ->
text_utils:ustring().
get_line( Prompt, GetLineScriptPath ) ->
% Having the script display the prompt would not work, as that script would
% not be able to write to the standard input (1):
%
%Cmd = text_utils:format( "get-line-as-external-program.sh \"~s\" 1>&4",
% [ Prompt ] ),
io:format( Prompt ),
% We have to execute a real executable (ex: not a shell builtin):
Cmd = GetLineScriptPath ++ " 1>&4",
Env = system_utils:get_standard_environment(),
PortOpts = [ stream, nouse_stdio, exit_status, eof ],
case system_utils:run_executable( Cmd, Env, _WorkingDir=undefined,
PortOpts ) of
{ _ExitStatus=0, UserText } ->
UserText;
{ ExitStatus, Any } ->
throw( { myriad_get_line_failed, ExitStatus, Any } )
end.
% Returns the path to the Myriad helper script for get_line/1 operations.
-spec get_line_helper_script() -> file_utils:executable_path().
get_line_helper_script() ->
GetLineScript = file_utils:join( script_utils:get_script_base_directory(),
"get-line-as-external-program.sh" ),
case file_utils:is_existing_file( GetLineScript ) of
true ->
case file_utils:is_executable( GetLineScript ) of
true ->
GetLineScript;
false ->
throw( { script_not_executable, GetLineScript } )
end;
false ->
throw( { script_not_found, GetLineScript } )
end.
% Returns a default, standard environment for "porcelain"-like executions,
% i.e. executions that are, as much as possible, reproducible in various runtime
% contexts (typically: with locale-independent outputs).
%
% To be used with run_executable/{2,3}.
%
-spec get_standard_environment() -> environment().
get_standard_environment() ->
[ { "LANG", "C" } ].
% Monitors a port: reads command data and signals from a port, and reports it.
monitor_port( Port, Data ) ->
%trace_utils:debug_fmt( "Process ~p starting the monitoring of "
% "port ~p (data: '~p').", [ self(), Port, Data ] ),
receive
{ Port, { data, NewData } } ->
%trace_utils:debug_fmt( "Port monitor ~p received data: '~p'.",
% [ self(), NewData ] ),
monitor_port( Port, [ NewData | Data ] );
% As mentioned in the documentation, "the eof message and the
% exit_status message appear in an unspecified order":
{ Port, eof } ->
%trace_utils:debug_fmt( "Port monitor ~p received eof (first).",
% [ self() ] ),
port_close( Port ),
receive
{ Port, { exit_status, ExitStatus } } ->
%trace_utils:debug_fmt( "Port monitor ~p received "
% "exit_status (second).", [ self() ] ),
% Otherwise we have an enclosing list and last character is
% always "\n":
%
Output = text_utils:remove_ending_carriage_return(
lists:flatten( lists:reverse( Data ) ) ),
{ ExitStatus, Output }
end;
{ Port, { exit_status, ExitStatus } } ->
%trace_utils:debug_fmt( "Port monitor ~p received "
% "exit_status (first).", [ self() ] ),
receive
{ Port, eof } ->
%trace_utils:debug( "Port monitor ~p received eof "
% "(second).", [ self() ] ),
port_close( Port ),
Output = text_utils:remove_ending_carriage_return(
lists:flatten( lists:reverse( Data ) ) ),
{ ExitStatus, Output }
end;
% Added by ourselves so that we can avoid process leakage:
terminate_port ->
%trace_utils:debug_fmt( "Port monitor ~p terminating.",
% [ self() ] ),
% Anyway no PID to send information to:
port_terminated
end.
% Evaluates specified shell (ex: sh, bash, etc. - not Erlang) expression, in
% a standard environment.
%
% No return code is available with this approach, only the output of the
% expression.
%
-spec evaluate_shell_expression( shell_expression() ) -> expression_outcome().
evaluate_shell_expression( Expression ) ->
evaluate_shell_expression( Expression, get_standard_environment() ).
% Evaluates specified shell (ex: sh, bash, etc. - not Erlang) expression, in
% specified environment.
%
% No return code is available with this approach, only the output of the
% expression.
%
-spec evaluate_shell_expression( shell_expression(), environment() ) ->
expression_outcome().
evaluate_shell_expression( Expression, Environment ) ->
FullExpression = get_actual_expression( Expression, Environment ),
%trace_utils:debug_fmt( "Evaluation shell expression '~s' "
% "in ~s", [ FullExpression,
% environment_to_string( Environment ) ] ),
% No return code available, success supposed:
text_utils:remove_ending_carriage_return( os:cmd( FullExpression ) ).
% No evaluate_shell_expression/3 defined, as one may change the current working
% directory directly from the shell expression.
% Executes asynchronously, in the background, specified executable (with no
% specific parameter nor port option, and from the current directory), in a
% standard shell environment.
%
% As a consequence it returns no return code (exit status) nor output.
%
% For that, as it is a process-blocking operation in Erlang, a dedicated process
% is spawned (and most probably lost).
%
% If this function is expected to be called many times, to avoid the process
% leak, one should consider using evaluate_background_shell_expression/1
% instead.
%
-spec run_background_executable( command() ) -> void().
run_background_executable( ExecPath ) ->
run_background_executable( ExecPath, get_standard_environment() ).
% Executes asynchronously, in the background, specified executable (with no
% specific parameter nor port option, and from the current directory), in the
% specified shell environment.
%
% As a consequence it returns no return code (exit status) nor output.
%
% For that, as it is a process-blocking operation in Erlang, a dedicated process
% is spawned (and most probably lost).
%
% If this function is expected to be called many times, to avoid the process
% leak, one should consider using evaluate_background_shell_expression/2
% instead.
%
-spec run_background_executable( command(),
environment() ) -> void().
run_background_executable( ExecPath, Environment ) ->
run_background_executable( ExecPath, Environment, _WorkingDir=undefined ).
% Executes asynchronously, in the background, specified executable (with no
% specific parameter nor port option), in the specified shell environment and
% from the specified working directory.
%
% As a consequence it returns no return code (exit status) nor output.
%
% For that, as it is a process-blocking operation in Erlang, a dedicated process
% is spawned (and most probably lost).
%
% If this function is expected to be called many times, to avoid the process
% leak, one should consider using evaluate_background_shell_expression/2
% instead.
%
-spec run_background_executable( command(), environment(),
maybe( working_dir() ) ) -> void().
run_background_executable( ExecPath, Environment, MaybeWorkingDir ) ->
run_background_executable( ExecPath, Environment, MaybeWorkingDir,
_PortOptions=[] ).
% Executes asynchronously, in the background, specified executable, in the
% specified shell environment and working directory, with specified options
% (possibly containing any relevant command-line arguments; see
% http://erlang.org/doc/man/erlang.html#open_port-2).
%
% As a consequence it returns no return code (exit status) nor output.
%
% For that, as it is a process-blocking operation in Erlang, a dedicated process
% is spawned (and most probably lost).
%
% If this function is expected to be called many times, to avoid the process
% leak, one should consider using evaluate_background_shell_expression/2
% instead.
%
-spec run_background_executable( command(), environment(),
maybe( working_dir() ), [ port_option() ] ) -> void().
run_background_executable( Command, Environment, MaybeWorkingDir,
PortOptions ) ->
%trace_utils:debug_fmt( "Running executable '~s' with ~s "
% "from working directory '~s', with options ~p.",
% [ ExecPath, environment_to_string( Environment ), MaybeWorkingDir,
% PortOptions ] ),
% Apparently using a port-based launch and a background execution will block
% the current process, so we sacrifice a process here - yet we monitor it:
%
?myriad_spawn_link( fun() ->
ExecOutcome = run_executable( Command, Environment,
MaybeWorkingDir, PortOptions ),
% Does not seem to be ever executed:
trace_utils:debug_fmt( "Execution outcome: ~p.",
[ ExecOutcome ] )
end ).
% Executes asynchronously, in the background, specified shell expression with
% specified environment, in current directory.
%
% As a consequence it returns no return code (exit status) nor output.
%
-spec evaluate_background_shell_expression( shell_expression() ) -> void().
evaluate_background_shell_expression( Expression ) ->
evaluate_background_shell_expression( Expression,
get_standard_environment() ).
% Executes asynchronously, in the background, specified shell command with
% specified environment, in current directory.
%
% As a consequence it returns no return code (exit status) nor output.
%
-spec evaluate_background_shell_expression( shell_expression(),
environment() ) -> void().
evaluate_background_shell_expression( Expression, Environment ) ->
FullExpression = get_actual_expression( Expression, Environment ),
%trace_utils:debug_fmt(
% "Evaluating in the background following shell expression: '~s'.",
% [ FullExpression ] ),
os:cmd( FullExpression ++ " &" ).
% Returns a string that can be used as a shell prefix for commands, based on
% specified environment.
%
-spec get_environment_prefix( environment() ) -> string().
get_environment_prefix( Environment ) ->
% We do not specifically *unset* a variable whose value is false, we set it
% to an empty string:
%
VariableStrings = [ begin
ActualValue = case Value of
false ->
"";
_ ->
Value
end,
io_lib:format( "~s=~s", [ Name, ActualValue ] )
end || { Name, Value } <- Environment ],
text_utils:join( _Separator=" ", VariableStrings ).
% Returns the full, actual shell expression corresponding to specified
% expression and environment.
%
-spec get_actual_expression( shell_expression(), environment() ) ->
expression_outcome().
get_actual_expression( Expression, _Environment=[] ) ->
% Allows to avoid starting the command with a space:
Expression;
get_actual_expression( Expression, Environment ) ->
get_environment_prefix( Environment ) ++ " " ++ Expression.
% Returns the value associated to the specified environment variable (if any),
% otherwise 'false'.
%
-spec get_environment_variable( env_variable_name() ) -> env_variable_value().
get_environment_variable( VarName ) ->
os:getenv( VarName ).
% Sets the specified environment variable to the specified value, possibly
% overwriting a past value.
%
-spec set_environment_variable( env_variable_name(), env_variable_value() ) ->
void().
set_environment_variable( VarName, VarValue ) ->
os:putenv( VarName, VarValue ).
% Returns the current shell environment, sorted by variable names.
-spec get_environment() -> environment().
get_environment() ->
StringEnv = lists:sort( os:getenv() ),
[ text_utils:split_at_first( $=, VarEqValue ) || VarEqValue <-StringEnv ].
% Returns a textual description of the current shell environment.
-spec environment_to_string() -> string().
environment_to_string() ->
environment_to_string( get_environment() ).
% Returns a textual description of the specified shell environment.
-spec environment_to_string( environment() ) -> string().
environment_to_string( _Environment=[] ) ->
"an empty shell environment";
environment_to_string( Environment ) ->
{ SetVars, UnsetVars } = lists:partition(
fun( { _Name, _Value=false } ) ->
false;
( _ ) ->
true
end,
Environment ),
VariableStrings = [ text_utils:format( "~s = ~s", [ Name, Value ] )
|| { Name, Value } <- SetVars ],
FinalVariableStrings = case UnsetVars of
[] ->
VariableStrings;
_ ->
UnsetNames = [ Name || { Name, _False } <- UnsetVars ],
UnsetString = "unset variables: "
++ text_utils:join( _Sep=", ", UnsetNames ),
list_utils:append_at_end( UnsetString, VariableStrings )
end,
"following shell environment: "
++ text_utils:strings_to_string( FinalVariableStrings ).
% Returns the version informations of the current Erlang interpreter (actually
% the environment one, including the VM) being used.
%
% Returns a full version name (ex: "R13B04") or, if not available, a shorter one
% (ex: "R11B").
%
-spec get_interpreter_version() -> string().
get_interpreter_version() ->
% Older versions (pre-R13A?) did not support the otp_release tag:
try erlang:system_info( otp_release ) of
StringVersion ->
try list_to_integer( StringVersion ) of
V ->
% Ex: V=17 Newer release (ex: 17.0-rc1) do not comply to the
% traditional scheme, applying it for uniformity and maybe a
% bit of nostalgia:
%
lists:flatten( io_lib:format( "R~BB", [ V ] ) )
catch
_:_ ->
% Ex: StringVersion="R13B04":
StringVersion
end
catch
_:_ ->
% Here we revert to another (older) solution:
{ _OTPInfos, StringVersion } = init:script_id(),
% Ex: StringVersion="R11B"
StringVersion
end.
% Returns the version information (as a 2 or 3-part tuple) corresponding to the
% specified Erlang application (ex: for 'kernel', could return {3,0} or
% {2,16,3}).
%
% Throws an exception if the information could not be retrieved.
%
-spec get_application_version( atom() ) -> basic_utils:any_version().
get_application_version( Application ) ->
case application:get_key( Application, vsn ) of
% Ex: "3.0" or "2.16.3":
{ ok, VsnString } ->
basic_utils:parse_version( VsnString );
undefined ->
throw( { application_version_not_found, Application } )
end.
% Returns the size, in bytes, of a word of this Virtual Machine.
-spec get_size_of_vm_word() -> basic_utils:count().
get_size_of_vm_word() ->
erlang:system_info( wordsize ).
% Returns a textual description of the size of a VM word.
%
% Cannot crash.
%
-spec get_size_of_vm_word_string() -> text_utils:ustring().
get_size_of_vm_word_string() ->
try
io_lib:format( "size of a VM word: ~B bytes",
[ get_size_of_vm_word() ] )
catch _AnyClass:Exception ->
io_lib:format( "size of a VM word could not be obtained (~p)",
[ Exception ] )
end.
% Returns the size of specified term, in bytes, in the heap.
%
% Note that off-heap data (such as binaries larger than 64 bytes) is not counted
% here. The (flat) size is incremented to account for the top term word (which
% is kept in a register or on the stack).
%
-spec get_size( term() ) -> byte_size().
get_size( Term ) ->
% With sharing taken into account:
% use ( erts_debug:size( Term ) + 1 ) * get_size_of_vm_word()
%
( erts_debug:flat_size( Term ) + 1 ) * get_size_of_vm_word().
% Returns a string containing a user-friendly description of the specified size
% expressed in bytes, using GiB (Gibibytes, not Gigabytes), MiB (Mebibytes, not
% Megabytes), KiB (Kibibytes, not Kilobytes) and bytes.
%
% See http://en.wikipedia.org/wiki/Kibibyte
%
-spec interpret_byte_size( byte_size() ) -> string().
interpret_byte_size( SizeInBytes ) ->
Kilo = 1024,
Mega = Kilo*Kilo,
Giga = Kilo*Mega,
ListWithGiga = case SizeInBytes div Giga of
0 ->
[];
GigaNonNull->
[ io_lib:format( "~B GiB", [ GigaNonNull ] ) ]
end,
SizeAfterGiga = SizeInBytes rem Giga,
%io:format( "SizeAfterGiga = ~B.~n", [ SizeAfterGiga ] ),
ListWithMega = case SizeAfterGiga div Mega of
0 ->
ListWithGiga;
MegaNonNull->
[ io_lib:format( "~B MiB", [ MegaNonNull ] ) | ListWithGiga ]
end,
SizeAfterMega = SizeAfterGiga rem Mega,
%io:format( "SizeAfterMega = ~B.~n", [ SizeAfterMega ] ),
ListWithKilo = case SizeAfterMega div Kilo of
0 ->
ListWithMega;
KiloNonNull->
[ io_lib:format( "~B KiB", [ KiloNonNull ] ) | ListWithMega ]
end,
SizeAfterKilo = SizeAfterMega rem Kilo,
%io:format( "SizeAfterKilo = ~B.~n", [ SizeAfterKilo ] ),
ListWithByte = case SizeAfterKilo rem Kilo of
0 ->
ListWithKilo ;
1->
[ "1 byte" | ListWithKilo ];
AtLeastTwoBytes ->
[ io_lib:format( "~B bytes", [ AtLeastTwoBytes ] ) | ListWithKilo ]
end,
%io:format( "Unit list is: ~w.~n", [ ListWithByte ] ),
case ListWithByte of
[] ->
"0 byte";
[ OneElement ] ->
OneElement;
[ Smaller | Bigger ] ->
text_utils:join( ", ", lists:reverse( Bigger ) )
++ " and " ++ Smaller
end.
% Returns a string containing a user-friendly description of the specified size
% expressed in bytes, using the most appropriate unit among GiB (Gibibytes, not
% Gigabytes), MiB (Mebibytes, not Megabytes), KiB (Kibibytes, not Kilobytes) and
% bytes, rounding that value to 1 figure after the comma (this is thus an
% approximate value).
%
% See http://en.wikipedia.org/wiki/Kibibyte
%
-spec interpret_byte_size_with_unit( byte_size() ) -> string().
interpret_byte_size_with_unit( Size ) ->
{ Unit, Value } = convert_byte_size_with_unit( Size ),
case Unit of
byte ->
case Value of
0 ->
"0 byte";
1 ->
"1 byte";
Other ->
io_lib:format( "~B bytes", [ Other ] )
end;
kib ->
io_lib:format( "~.1f KiB", [ Value ] );
mib ->
io_lib:format( "~.1f MiB", [ Value ] );
gib ->
io_lib:format( "~.1f GiB", [ Value ] )
end.
% Converts the specified size, in bytes, as a value expressed in an appropriate
% size unit.
%
% Returns a { Unit, Value } pair, in which:
%
% - Unit is the largest size unit that can be selected so that the specified
% size if worth at least 1 unit of it (ex: we do not want a value 0.9, at least
% 1.0 is wanted); Unit can be 'gib', for GiB (Gibibytes), 'mib', for MiB
% (Mebibytes), 'kib' for KiB (Kibibytes), or 'byte', for Byte
%
% - Value is the converted byte size, in the specified returned unit, expressed
% either as an integer (for bytes) or as a float
%
% Ex: 1023 (bytes) translates to { byte, 1023 }, 1025 translates to
% { kib, 1.0009765625 }.
%
% Note that the returned value cannot be expected to be exact (rounded),
% therefore this function is mostly useful for user output.
%
-spec convert_byte_size_with_unit( byte_size() ) ->
{ 'byte', integer() } | { 'kib', float() } | { 'mib', float() }
| { 'gib', float() }.
convert_byte_size_with_unit( SizeInBytes ) ->
Kilo = 1024,
Mega = Kilo*Kilo,
Giga = Kilo*Mega,
case SizeInBytes div Giga of
0 ->
case SizeInBytes div Mega of
0 ->
case SizeInBytes div Kilo of
0 ->
%{ byte, float( SizeInBytes ) };
{ byte, SizeInBytes };
_ ->
{ kib, SizeInBytes / Kilo }
end;
_ ->
{ mib, SizeInBytes/Mega }
end;
_ ->
{ gib, SizeInBytes / Giga }
end.
% Returns a summary of the dynamically allocated memory currently being used by
% the Erlang emulator.
%
-spec display_memory_summary() -> void().
display_memory_summary() ->
SysSize = erlang:memory( system ),
ProcSize = erlang:memory( processes ),
Sum = SysSize + ProcSize,
io:format( " - system size: ~s (~s)~n",
[ interpret_byte_size_with_unit( SysSize ),
text_utils:percent_to_string( SysSize / Sum ) ] ),
io:format( " - process size: ~s (~s)~n",
[ interpret_byte_size_with_unit( ProcSize ),
text_utils:percent_to_string( ProcSize / Sum ) ] ).
% Returns the total installed physical volatile memory (RAM) of the local
% computer, expressed in bytes.
%
-spec get_total_physical_memory() -> byte_size().
get_total_physical_memory() ->
% First check the expected unit is returned, by pattern-matching:
UnitCommand = ?cat "/proc/meminfo |" ?grep "'MemTotal:' |"
?awk "'{print $3}'",
case run_executable( UnitCommand ) of
{ _ExitCode=0, _Output="kB" } ->
% Ok, using kB indeed.
ValueCommand = ?cat "/proc/meminfo |" ?grep "'MemTotal:' |"
?awk "'{print $2}'",
% The returned value of following command is like "12345\n", in
% bytes:
%
case run_executable( ValueCommand ) of
{ _ExitCode=0, MemSizeString } ->
% They were probably kiB:
list_to_integer( MemSizeString ) * 1024;
{ ExitCode, ErrorOutput } ->
throw( { total_physical_memory_inquiry_failed, ExitCode,
ErrorOutput } )
end;
{ ExitCode, ErrorOutput } ->
throw( { total_physical_memory_inquiry_failed, ExitCode,
ErrorOutput } )
end.
% Returns a textual description of the total installed physical memory.
%
% Cannot crash.
%
-spec get_total_physical_memory_string() -> text_utils:ustring().
get_total_physical_memory_string() ->
try
io_lib:format( "total physical memory: ~ts",
[ interpret_byte_size( get_total_physical_memory() ) ] )
catch _AnyClass:Exception ->
io_lib:format( "no total physical RAM information could be "
"obtained (~p)", [ Exception ] )
end.
% Returns the total installed physical volatile memory (RAM) of the computer on
% which specified node (specified as an atom) is running, expressed in bytes.
%
-spec get_total_physical_memory_on( net_utils:atom_node_name() ) -> byte_size().
get_total_physical_memory_on( Node ) ->
% No standard environment enforced here.
% First check the expected unit is returned, by pattern-matching:
UnitCommand = ?cat "/proc/meminfo |" ?grep "'MemTotal:' |"
?awk "'{print $3}'",
"kB\n" = rpc:call( Node, os, cmd, [ UnitCommand ] ),
ValueCommand = ?cat "/proc/meminfo |" ?grep "'MemTotal:' |"
?awk "'{print $2}'",
ValueCommandOutput = rpc:call( Node, os, cmd, [ ValueCommand ] ),
% The returned value of following command is like "12345\n", in bytes:
MemorySizeString = text_utils:remove_ending_carriage_return(
ValueCommandOutput ),
% They were probably kiB:
list_to_integer( MemorySizeString ) * 1024.
% Returns the total memory used, in bytes, by this instance of the Erlang VM,
% i.e. the total amount of memory currently allocated by the Erlang processes
% and by this emulator.
%
-spec get_memory_used_by_vm() -> byte_size().
get_memory_used_by_vm() ->
erlang:memory( total ).
% Returns { UsedRAM, TotalRAM } where UsedRAM is the actual total memory used on
% the current host by all applications, in bytes, and TotalRAM is the total
% installed RAM, in bytes.
%
% The cached memory and the buffers used by the kernel are not taken into
% account into the returned count.
%
-spec get_total_memory_used() -> { byte_size(), byte_size() }.
get_total_memory_used() ->
% Example of memory information as returned by the 'free' command:
% (slightly edited with variable names)
%
% """
% total used free shared buffers cached
% Mem: A=8202424 B=5588920 C=2613504 D=0 E=567480 F=3212392
% -/+ buffers/cache: G=1809048 H=6393376
% """
%
% We have: H = C + D + E + F, and G = A - H. D is never used (obsolete).
% We return here { G, A }, thus { G, G+H }.
% Avoid locale and greps 'buffers/cache:' (ex: on Debian) as well as
% 'buff/cache' (ex: on Arch)
%MemoryInfo = os:cmd( "LANG= free -b | grep '/cache' "
% "| awk '{print $3,$4}'" ),
% Converts MemoryInfo from "a b\n" to ["a","b\n"]
%[ AppliUsedString, TotalFreeTermString ] =
% string:tokens( MemoryInfo, " " ),
% Unfortunately on Arch we have quite different outputs, like:
% total used free shared buff/cache available
% Mem: 8047428 2476488 1124396 362228 4446544 4893712
% Swap: 0 0 0
% This is G:
%AppliUsedSize = text_utils:string_to_integer( AppliUsedString ),
%TotalFreeString = text_utils:remove_ending_carriage_return(
% TotalFreeTermString ),
% This is H:
%TotalFreeSize = text_utils:string_to_integer( TotalFreeString ),
% { G, G+H }:
%{ AppliUsedSize, AppliUsedSize + TotalFreeSize }.
% So finally we preferred /proc/meminfo, used first to get MemTotal:
%
TotalString = case run_executable( ?cat "/proc/meminfo |"
?grep "'^MemTotal:' |" ?awk "'{print $2,$3}'" ) of
{ _TotalExitCode=0, TotalOutput } ->
%io:format( "TotalOutput: '~p'~n", [ TotalOutput ] ),
TotalOutput;
{ TotalExitCode, TotalErrorOutput } ->
throw( { total_memory_used_inquiry_failed, TotalExitCode,
TotalErrorOutput } )
end,
[ Total, "kB" ] = string:tokens( TotalString, " " ),
TotalByte = text_utils:string_to_integer( Total ) * 1024,
% MemAvailable does not seem always available:
%
FreeString = case run_executable(
?cat "/proc/meminfo |" ?grep "'^MemAvailable:' |"
?awk "'{print $2,$3}'" ) of
{ _AvailExitCode=0, MemAvailOutput } ->
%io:format( "## using MemAvailable~n" ),
MemAvailOutput;
{ _AvailExitCode, _AvailErrorOutput } ->
% In some cases (ex: Debian 6.0), no 'MemAvailable' is defined, we
% use 'MemFree' instead (we consider they are synonymous):
%io:format( "## using MemFree~n" ),
case run_executable( ?cat "/proc/meminfo |" ?grep "'^MemFree:' |"
?awk "'{print $2,$3}'" ) of
{ _FreeExitCode=0, MemFreeOutput } ->
MemFreeOutput;
{ FreeExitCode, FreeErrorOutput } ->
throw( { total_memory_used_inquiry_failed, FreeExitCode,
FreeErrorOutput } )
end
end,
% The problem is that even if MemAvailable is not found, we have a zero exit
% code (and an empty string):
%
FreeByte = case FreeString of
[] ->
% As a last resort we do as before, i.e. we use free:
%io:format( "## using free~n" ),
case run_executable(
?free "-b |" ?grep "'/cache' |" ?awk "'{print $3}'" ) of
{ _ExitCode=0, FreeOutput } ->
% Already in bytes:
text_utils:string_to_integer( FreeOutput );
{ ExitCode, ErrorOutput } ->
throw( { total_memory_used_inquiry_failed, ExitCode,
ErrorOutput } )
end;
_ ->
[ Free, "kB" ] = string:tokens( FreeString, " " ),
text_utils:string_to_integer( Free ) * 1024
end,
UsedByte = TotalByte - FreeByte,
{ UsedByte, TotalByte }.
% Returns a textual description of the current RAM status.
%
% Cannot crash.
%
-spec get_ram_status_string() -> text_utils:ustring().
get_ram_status_string() ->
try
case get_total_memory_used() of
{ _UsedRAM, _TotalRAM=0 } ->
"total RAM size could not be obtained (found null)";
{ UsedRAM, TotalRAM } ->
io_lib:format( "RAM memory used: ~s, over a total of ~s (~s)",
[ interpret_byte_size( UsedRAM ),
interpret_byte_size( TotalRAM ),
text_utils:percent_to_string( UsedRAM / TotalRAM ) ] )
end
catch _AnyClass:Exception ->
io_lib:format( "no RAM information could be obtained (~p)",
[ Exception ] )
end.
% Returns { UsedSwap, TotalSwap } where UsedSwap is the size of the used swap
% and TotalSwap is the total amount of swap space on the local host, both
% expressed in bytes.
%
% Will crash if the information cannot be retrieved properly.
%
-spec get_swap_status() -> { byte_size(), byte_size() }.
get_swap_status() ->
% Same reason as for get_total_memory_used/0:
%SwapInfos = os:cmd( "free -b | grep 'Swap:' | awk '{print $2, $3}'" ),
SwapTotalString = case run_executable( ?cat "/proc/meminfo |"
?grep "'^SwapTotal:' |" ?awk "'{print $2,$3}'" ) of
{ _TotalExitCode=0, TotalOutput } ->
TotalOutput;
{ TotalExitCode, TotalErrorOutput } ->
throw( { swap_inquiry_failed, TotalExitCode, TotalErrorOutput } )
end,
[ TotalString, "kB" ] = string:tokens( SwapTotalString, " " ),
TotalByte = text_utils:string_to_integer( TotalString ) * 1024,
SwapFreeString = case run_executable(
?cat "/proc/meminfo |" ?grep "'^SwapFree:' |"
?awk "'{print $2,$3}'" ) of
{ _FreeExitCode=0, FreeOutput } ->
FreeOutput;
{ FreeExitCode, FreeErrorOutput } ->
throw( { swap_inquiry_failed, FreeExitCode, FreeErrorOutput } )
end,
[ FreeString, "kB" ] = string:tokens( SwapFreeString, " " ),
FreeByte = text_utils:string_to_integer( FreeString ) * 1024,
UsedByte = TotalByte - FreeByte,
{ UsedByte, TotalByte }.
% Returns a textual description of the current swap status.
%
% Cannot crash.
%
-spec get_swap_status_string() -> text_utils:ustring().
get_swap_status_string() ->
try
case get_swap_status() of
{ _UsedSwap, _TotalSwap=0 } ->
"no swap found";
{ UsedSwap, TotalSwap } ->
io_lib:format( "swap used: ~s over a total of ~s (~s)",
[ interpret_byte_size( UsedSwap ),
interpret_byte_size( TotalSwap ),
text_utils:percent_to_string(
UsedSwap / TotalSwap ) ] )
end
catch _AnyClass:Exception ->
io_lib:format( "no swap information could be obtained (~p)",
[ Exception ] )
end.
% Returns the number of cores available on the local host.
%
% Throws an exception on failure.
%
-spec get_core_count() -> integer().
get_core_count() ->
CoreString = case run_executable(
?cat "/proc/cpuinfo |" ?grep "-c processor" ) of
{ _ExitCode=0, Output } ->
Output;
{ ExitCode, ErrorOutput } ->
throw( { core_count_inquiry_failed, ExitCode, ErrorOutput } )
end,
try
text_utils:string_to_integer( CoreString )
catch
{ integer_conversion_failed, CoreString } ->
throw( { could_not_determine_core_count, CoreString } )
end.
% Returns a textual description of the number of the local processing cores.
%
% Cannot crash.
%
-spec get_core_count_string() -> text_utils:ustring().
get_core_count_string() ->
try
io_lib:format( "number of cores: ~B", [ get_core_count() ] )
catch _AnyClass:Exception ->
io_lib:format( "no core information could be obtained (~p)",
[ Exception ] )
end.
% Returns the number of live Erlang processes on the current node.
-spec get_process_count() -> basic_utils:count().
get_process_count() ->
erlang:system_info( process_count ).
% Returns a textual description of the number of live Erlang processes on the
% current node.
%
% Cannot crash.
%
-spec get_process_count_string() -> text_utils:ustring().
get_process_count_string() ->
try
io_lib:format( "number of existing Erlang processes: ~B ",
[ get_process_count() ] )
catch _AnyClass:Exception ->
io_lib:format( "no information about the number of live Erlang "
"processes could be obtained (~p)", [ Exception ] )
end.
% Returns an aggregated view of the CPU usage (a float in [0;100]) based on the
% two specified sets of CPU counters, i.e. the average (on all cores of all
% processors of the local host) percentage of CPU utilization (all kinds of
% usage except idle) during the period which elapsed between the start and end
% measures (in that order).
%
% Typical usage:
%
% FirstMeasure = system_utils:get_cpu_usage_counters(),
% (do something)
% SecondMeasure = system_utils:get_cpu_usage_counters(),
%
% UsageInPercent = system_utils:compute_cpu_usage_between( FirstMeasure,
% SecondMeasure )
%
-spec compute_cpu_usage_between( cpu_usage_info(), cpu_usage_info() ) ->
math_utils:percent().
compute_cpu_usage_between( StartCounters, EndCounters ) ->
Percentages = compute_detailed_cpu_usage( StartCounters, EndCounters ),
compute_cpu_usage_for( Percentages ).
% Returns an aggregated view of the CPU usage (a float in [0;100]) based on the
% specified detailed CPU percentages, i.e. the average (on all cores of all
% processors of the local host) percentage of CPU utilization (all kinds of
% usage except idle) during the period the input percentages correspond to.
%
% Returns 'undefined' iff the specified usage is itself undefined.
%
-spec compute_cpu_usage_for( maybe( cpu_usage_percentages() ) ) ->
maybe( math_utils:percent() ).
compute_cpu_usage_for( undefined ) ->
undefined;
compute_cpu_usage_for( { UserPercent, NicePercent, SystemPercent, _IdlePercent,
OtherPercent } ) ->
% Every usage matters here, except idle:
UserPercent + NicePercent + SystemPercent + OtherPercent.
% Returns a detailed view of the CPU usage, i.e. the average (on all cores of
% all processors of the local host) percentage of the various kinds of CPU
% utilization: { UserPercent, NicePercent, SystemPercent, IdlePercent,
% OtherPercent }, respectively for user mode, user mode with low priority
% (nice), system mode, idle task and all other usages (if any), between the two
% sets of measures.
%
% If the two sets of specified counters are equal, returns 'undefined', as no
% usage can be quantified then.
%
-spec compute_detailed_cpu_usage( cpu_usage_info(), cpu_usage_info() ) ->
maybe( cpu_usage_percentages() ).
compute_detailed_cpu_usage( _StartCounters={ U1, N1, S1, I1, O1 },
_EndCounters = { U2, N2, S2, I2, O2 } ) ->
User = U2 - U1,
Nice = N2 - N1,
System = S2 - S1,
Idle = I2 - I1,
Other = O2 - O1,
% This would be great if we could avoid a division by zero:
case User + Nice + System + Idle + Other of
% Yes, this happens:
0.0 ->
undefined;
Sum ->
RoundDigits = 1,
UserPercent = math_utils:round_after( 100 * User / Sum,
RoundDigits ),
NicePercent = math_utils:round_after( 100 * Nice / Sum,
RoundDigits ),
SystemPercent = math_utils:round_after( 100 * System / Sum,
RoundDigits ),
IdlePercent = math_utils:round_after( 100 * Idle / Sum,
RoundDigits ),
AllButOtherPercent = UserPercent + NicePercent + SystemPercent
+ IdlePercent,
% Avoids rounding errors:
OtherPercent = math_utils:round_after( 100 - AllButOtherPercent,
RoundDigits ),
{ UserPercent, NicePercent, SystemPercent, IdlePercent,
OtherPercent }
end.
% Returns the instantaneous CPU counters, as maintained from boot.
%
% Note: mostly useful in terms of differences over time.
%
-spec get_cpu_usage_counters() -> cpu_usage_info().
get_cpu_usage_counters() ->
% grep more versatile than: '| head -n 1':
StatString = case run_executable(
?cat "/proc/stat |" ?grep "'cpu '" ) of
{ _ExitCode=0, Output } ->
Output;
{ ExitCode, ErrorOutput } ->
throw( { cpu_counters_inquiry_failed, ExitCode, ErrorOutput } )
end,
% Ex: cpu 1331302 11435 364777 150663306 82509 249 3645 0 0
% Tells the time spent in user mode, user mode with low priority (nice),
% system mode, and the idle task.
[ "cpu", UserString, NiceString, SystemString, IdleString | T ] =
string:tokens( StatString, " " ),
User = text_utils:string_to_integer( UserString ),
Nice = text_utils:string_to_integer( NiceString ),
System = text_utils:string_to_integer( SystemString ),
Idle = text_utils:string_to_integer( IdleString ),
% Adapts to any architecture and update (iowait, irq, softirq, steal, guest,
% etc.):
Other = lists:sum( [ text_utils:string_to_integer( E ) || E <- T ] ),
%io:format( "user = ~f, nice = ~f, system = ~f, idle = ~f, other = ~f, "
% "T = ~p~n", [ User, Nice, System, Idle, Other, T ] ),
{ User, Nice, System, Idle, Other }.
% Returns the current usage of disks, as a human-readable string.
-spec get_disk_usage() -> text_utils:ustring().
get_disk_usage() ->
case run_executable( ?df "-h" ) of
{ _ExitCode=0, Output } ->
Output;
{ ExitCode, ErrorOutput } ->
throw( { disk_usage_inquiry_failed, ExitCode,
ErrorOutput } )
end.
% Returns a textual description of the current disk usage.
%
% Cannot crash.
%
-spec get_disk_usage_string() -> text_utils:ustring().
get_disk_usage_string() ->
try
io_lib:format( "current disk usage:~n~ts", [ get_disk_usage() ] )
catch _AnyClass:Exception ->
io_lib:format( "no disk usage information could be obtained (~p)",
[ Exception ] )
end.
% Returns a list of the known types of pseudo-filesystems.
-spec get_known_pseudo_filesystems() -> [ pseudo_filesystem_type() ].
get_known_pseudo_filesystems() ->
% A list of all current filesystems can be obtained thanks to: 'df -T'.
[ tmpfs, devtmpfs ].
% Returns a list of the current, local mount points (excluding the
% pseudo-filesystems).
%
-spec get_mount_points() -> [ file_utils:path() ].
get_mount_points() ->
FirstCmd = ?df "-h --local --output=target"
++ get_exclude_pseudo_fs_opt() ++ " |" ?grep "-v 'Mounted on'",
case run_executable( FirstCmd ) of
{ _FirstExitCode=0, ResAsOneString } ->
%io:format( "## using direct df~n" ),
text_utils:split( ResAsOneString, "\n" );
{ _FirstExitCode, _FirstErrorOutput } ->
% Older versions of df may not know the --output option:
SecondCmd = ?df "-h --local "
++ get_exclude_pseudo_fs_opt()
++ "| " ?grep "-v 'Mounted on' |" ?awk "'{print $6}'",
case run_executable( SecondCmd ) of
{ _SecondExitCode=0, ResAsOneString } ->
%io:format( "## using legacy df~n" ),
text_utils:split( ResAsOneString, "\n" );
{ SecondExitCode, SecondErrorOutput } ->
throw( { mount_point_inquiry_failed, SecondExitCode,
SecondErrorOutput } )
end
end.
% (helper for df)
get_exclude_pseudo_fs_opt() ->
Excludes = [ " --exclude-type=" ++ text_utils:atom_to_string( P )
|| P <- get_known_pseudo_filesystems() ],
text_utils:join( _Sep=" ", Excludes ).
% Returns information about the specified filesystem.
-spec get_filesystem_info( file_utils:bin_path() | file_utils:path() ) ->
fs_info().
get_filesystem_info( BinFilesystemPath ) when is_binary( BinFilesystemPath ) ->
get_filesystem_info( text_utils:binary_to_string( BinFilesystemPath ) );
get_filesystem_info( FilesystemPath ) ->
Cmd = ?df "--block-size=1K --local " ++ get_exclude_pseudo_fs_opt()
++ " --output=source,target,fstype,used,avail,iused,iavail '"
++ FilesystemPath ++ "' |" ?grep "-v 'Mounted on'",
case run_executable( Cmd ) of
{ _ExitCode=0, ResAsOneString } ->
% Order of the columns: 'Filesystem / Mounted on / Type / Used /
% Avail / IUsed / IFree':
%
case text_utils:split( ResAsOneString, " " ) of
[ Fs, Mount, Type, USize, ASize, Uinodes, Ainodes ] ->
%io:format( "## using direct df~n" ),
% df outputs kiB, not kB:
#fs_info{
filesystem=Fs,
mount_point=Mount,
type=get_filesystem_type( Type ),
used_size = 1024 * text_utils:string_to_integer( USize ),
available_size = 1024 *
text_utils:string_to_integer( ASize ),
used_inodes = text_utils:string_to_integer( Uinodes ),
available_inodes =
text_utils:string_to_integer( Ainodes )
};
_ ->
get_filesystem_info_alternate( FilesystemPath )
end;
{ _ExitCode, _ErrorOutput } ->
get_filesystem_info_alternate( FilesystemPath )
end.
% Alternate version, if the base version failed.
get_filesystem_info_alternate( FilesystemPath ) ->
% df must have failed, probably outdated and not understanding --output,
% defaulting to a less precise syntax:
%io:format( "## using alternate df~n" ),
Cmd = ?df "--block-size=1K --local "
++ get_exclude_pseudo_fs_opt() ++ " "
++ FilesystemPath ++ "|" ?grep "-v 'Mounted on'",
case run_executable( Cmd ) of
{ _ExitCode=0, ResAsOneString } ->
case text_utils:split( ResAsOneString, " " ) of
[ Fs, _1KBlocks, USize, ASize, _UsedPercent, Mount ] ->
% df outputs kiB, not kB:
#fs_info{
filesystem=Fs,
mount_point=Mount,
type=unknown,
used_size = 1024 * text_utils:string_to_integer( USize ),
available_size = 1024 *
text_utils:string_to_integer( ASize ),
used_inodes = 0,
available_inodes = 0
};
_ ->
throw( { filesystem_inquiry_failed, FilesystemPath,
ResAsOneString } )
end;
{ _ExitCode, ErrorOutput } ->
throw( { filesystem_inquiry_failed, FilesystemPath,
ErrorOutput } )
end.
% Returns a textual description of the specified filesystem information.
-spec filesystem_info_to_string( fs_info() ) -> text_utils:ustring().
filesystem_info_to_string( #fs_info{ filesystem=Fs, mount_point=Mount,
type=Type,
used_size=USize, available_size=ASize,
used_inodes=Uinodes,
available_inodes=Ainodes } ) ->
% For example vfat does not have inodes:
InodeString = case Uinodes + Ainodes of
0 ->
"";
S ->
Percent = 100 * Uinodes / S,
text_utils:format( ", hence used at ~.1f%", [ Percent ] )
end,
text_utils:format( "filesystem ~s mounted on ~s (type: ~s). "
"Used size: ~B bytes (i.e. ~s), available size: "
"~B bytes (i.e. ~s) hence used at ~.1f% "
"(total size: ~s), "
"using ~B inodes and having ~B of them available~s",
[ Fs, Mount, Type, USize,
interpret_byte_size_with_unit( USize ), ASize,
interpret_byte_size_with_unit( ASize ),
100 * USize / ( USize + ASize ),
interpret_byte_size_with_unit( USize + ASize ),
Uinodes, Ainodes, InodeString ] ).
-spec get_filesystem_type( text_utils:ustring() ) -> filesystem_type().
get_filesystem_type( TypeString ) ->
% Better for now than relying on an uncomplete list:
text_utils:string_to_atom( TypeString ).
% Returns a textual description of the current working directory.
%
% Cannot crash.
%
-spec get_current_directory_string() -> text_utils:ustring().
get_current_directory_string() ->
try
io_lib:format( "current directory: ~ts",
[ file_utils:get_current_directory() ] )
catch _AnyClass:Exception ->
io_lib:format( "no information about the current directory "
"could be obtained (~p)", [ Exception ] )
end.
% Returns a string describing the current operating system.
-spec get_operating_system_description() -> text_utils:ustring().
get_operating_system_description() ->
OSfile = "/etc/os-release",
case file_utils:is_existing_file_or_link( OSfile ) of
true ->
case run_executable( ?cat ++ OSfile ++ " |" ?grep "PRETTY_NAME |"
?sed "'s|^PRETTY_NAME=\"||1' |"
?sed "'s|\"$||1' 2>/dev/null" ) of
{ _ExitCode=0, Output } ->
Output;
{ _ExitCode, _ErrorOutput } ->
get_operating_system_description_alternate()
end;
false ->
get_operating_system_description_alternate()
end.
get_operating_system_description_alternate() ->
IdentifierPath = "/etc/issue.net",
case file_utils:is_existing_file( IdentifierPath ) of
true ->
BinString = file_utils:read_whole( IdentifierPath ),
text_utils:trim_whitespaces(
text_utils:binary_to_string( BinString ) );
false ->
"(unknown operating system)"
end.
% Returns a textual description of the operating system being used.
%
% Cannot crash.
%
-spec get_operating_system_description_string() -> text_utils:ustring().
get_operating_system_description_string() ->
try
io_lib:format( "operating system: ~ts",
[ get_operating_system_description() ] )
catch _AnyClass:Exception ->
io_lib:format( "no information about the operating system "
"could be obtained (~p)", [ Exception ] )
end.
% Returns a string describing the current state of the local system.
%
% Will not crash, even if some information could not be retrieved.
%
-spec get_system_description() -> string().
get_system_description() ->
% We use ~ts instead of ~s as in some cases, Unicode strings might be
% returned:
%
Subjects = [ get_core_count_string(),
get_size_of_vm_word_string(),
get_operating_system_description_string(),
get_process_count_string(),
get_total_physical_memory_string(),
get_ram_status_string(),
get_swap_status_string(),
get_user_name_string(),
get_user_home_directory_string(),
get_current_directory_string(),
get_disk_usage_string() ],
text_utils:strings_to_string( Subjects ).
% Tells whether this host has graphical output (typically a running X server).
-spec has_graphical_output() -> boolean().
has_graphical_output() ->
% Currently relying on this X-related variable:
case get_environment_variable( "DISPLAY" ) of
false ->
false;
_ ->
true
end.
% Prerequisite section.
% We suppose that by default all third-party dependencies (example taken here:
% the Foobar software) are conventionally installed under a common base
% directory, which is in turn conventionally named and located just under the
% user directory.
%
% More precisely, on Unix systems, our convention requests the base directory of
% all (third-party) dependencies to be '~/Software/'.
%
% We expect to have then the name of each prerequisite specified in CamelCase;
% ex: '~/Software/Foobar/'.
%
% Finally, each version thereof shall be installed in that directory (ex: a
% clone of the Foobar repository that could be named
% '~/Software/Foobar/foobar-20170601'), and be designated by a symbolic link
% named 'Foobar-current-install', still defined in '~/Software/Foobar'.
%
% As a result, one then can always access the current version of Foobar through
% the '~/Software/Foobar/Foobar-current-install' path (possibly pointing to
% successive versions thereof over time).
% Returns the (expected, conventional) base installation directory of the
% specified third-party, prerequisite package (ex: "Foobar").
%
-spec get_dependency_base_directory( package_name() ) ->
file_utils:directory_name().
get_dependency_base_directory( PackageName="ErlPort" ) ->
% ErlPort must be special-cased, as its actual base installation directory
% *must* be named "erlport" (otherwise the interpreter initialization may
% fail on new nodes with the {not_found,"erlport/priv"} error).
%
% So:
%
% - if the 'ERLPORT_BASE_DIR' environment variable is defined, and set to an
% existing directory, then this directory will be retained
%
% - otherwise a default will be used, corresponding to the
% '~/Software/ErlPort/erlport' directory
case get_environment_variable( "ERLPORT_BASE_DIR" ) of
false ->
% Then trying default path:
PathComponents = [ get_user_home_directory(), "Software",
PackageName, "erlport" ],
DefaultDir = file_utils:normalise_path(
file_utils:join( PathComponents ) ),
case file_utils:is_existing_directory_or_link( DefaultDir ) of
true ->
trace_utils:debug_fmt( "Using default Erlport directory "
"'~s'.", [ DefaultDir ] ),
DefaultDir;
false ->
trace_utils:error_fmt( "No Erlport installation found: the "
"ERLPORT_BASE_DIR environment variable is not defined, "
"and the default directory ('~s') does not exist.",
[ DefaultDir ] ),
throw( { erlport_default_directory_not_found, DefaultDir } )
end;
EnvDir ->
case filename:basename( EnvDir ) of
"erlport" ->
case file_utils:is_existing_directory_or_link( EnvDir ) of
true ->
trace_utils:debug_fmt( "Using the Erlport "
"directory specified in the ERLPORT_BASE_DIR "
"environment variable: '~s'.", [ EnvDir ] ),
EnvDir;
false ->
trace_utils:error_fmt( "The Erlport directory "
"specified in the ERLPORT_BASE_DIR environment "
"variable ('~s') does not exist.", [ EnvDir ] ),
throw( { erlport_specified_directory_not_found,
EnvDir } )
end;
_ ->
trace_utils:error_fmt( "The Erlport directory "
"specified in the ERLPORT_BASE_DIR environment "
"variable ('~s') does not end with 'erlport'.",
[ EnvDir ] ),
throw( { invalid_erlport_specified_directory, EnvDir } )
end
end;
get_dependency_base_directory( PackageName ) ->
% Expected to return a fully resolved version of the
% "$HOME/Software/Foobar/Foobar-current-install" path, such as
% "/home/stallone/Software/Foobar/Foobar-current-install":
%
PathComponents = [ get_user_home_directory(), "Software", PackageName,
PackageName ++ "-current-install" ],
file_utils:normalise_path( file_utils:join( PathComponents ) ).
% Returns the (expected, conventional) code installation directory of the
% specified third-party, prerequisite, Erlang package (ex: "Foobar").
%
-spec get_dependency_code_directory( package_name() ) ->
file_utils:directory_name().
get_dependency_code_directory( PackageName ) ->
% We would expect here
% /home/stallone/Software/Foobar/Foobar-current-install/ebin:
%
file_utils:join( get_dependency_base_directory( PackageName ), "ebin" ).
% Tells whether a JSON support is available.
-spec is_json_support_available() -> boolean().
is_json_support_available() ->
% This module can be built in all cases:
rest_utils:is_json_parser_available().
% Returns a string explaining what to do in order to have the JSON support
% available.
%
-spec get_json_unavailability_hint() -> string().
get_json_unavailability_hint() ->
"Hint: inspect, in myriad/GNUmakevars.inc, the USE_REST and "
"JSX_BASE variables, knowing that the current code path is: "
++ code_utils:get_code_path_as_string().
% Tells whether an HDF5 support is available.
-spec is_hdf5_support_available() -> boolean().
is_hdf5_support_available() ->
% Unlike dependencies like jsx whose compilation (in rest_utils.erl) do not
% need any specific *.hrl header (therefore rest_utils:start/0 is available
% in all cases), hdf5_support needs one (erlhdf5.hrl), hence the
% hdf5_support module may not be built at all, and thus will not be
% available even in order to provide a means of telling whether HDF can be
% supported.
%
% So:
%
case code_utils:is_beam_in_path( hdf5_support ) of
not_found ->
false;
_Paths ->
true
end.
% Returns a string explaining what to do in order to have the HDF5 support
% available.
%
-spec get_hdf5_unavailability_hint() -> string().
get_hdf5_unavailability_hint() ->
"Hint: inspect, in myriad/GNUmakevars.inc, the USE_HDF5 and "
"ERLHDF5_BASE variables.".