GajuMine/src/gmc_con.erl

419 lines
12 KiB
Erlang

%%% @doc
%%% GajuMine Controller
%%% @end
-module(gmc_con).
-vsn("0.3.1").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <craigeverett@qpq.swiss>").
-license("GPL-3.0-or-later").
-behavior(gen_server).
-export([start_stop/0, gajudesk/0]).
-export([conf/0, conf/1]).
-export([network/0]).
-export([start_link/0, stop/0]).
-export([init/1, terminate/2, code_change/3,
handle_call/3, handle_cast/2, handle_info/2]).
-include("$zx_include/zx_logger.hrl").
-include("$gajudesk_include/gd.hrl").
%%% Type and Record Definitions
-record(s,
{version = 1 :: integer(),
window = none :: none | wx:wx_object(),
network = <<"mainnet">> :: binary(), % <<"testnet">> | <<"mainnet">>
acc = none :: none | binary(),
keys = [] :: [],
max_cores = 2 :: pos_integer(),
max_mem = 3550722201 :: pos_integer(),
prefs = #{} :: #{},
gmc_conf = none :: none | reference()}).
-type state() :: #s{}.
%% Interface
-spec start_stop() -> ok.
start_stop() ->
gen_server:cast(?MODULE, start_stop).
-spec gajudesk() -> ok.
gajudesk() ->
gen_server:cast(?MODULE, gajudesk).
-spec conf() -> ok.
conf() ->
gen_server:cast(?MODULE, conf).
-spec conf({Account, Keys, Network, MaxCores, MaxMem}) -> ok
when Account :: none | binary(), % <<"ak_...">>
Keys :: [binary()], % [<<"ak_...">>]
Network :: binary(), % <<"mainnet">> | <<"testnet">> | <<"devnet">>
MaxCores :: none | integer(),
MaxMem :: none | integer().
conf(Info) ->
gen_server:cast(?MODULE, {conf, Info}).
-spec network() -> mainnet | testnet | none.
network() ->
gen_server:call(?MODULE, network).
-spec stop() -> ok.
stop() ->
gen_server:cast(?MODULE, stop).
%%% Startup Functions
-spec start_link() -> Result
when Result :: {ok, pid()}
| {error, Reason},
Reason :: {already_started, pid()}
| {shutdown, term()}
| term().
%% @private
%% Called by gmc_sup.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, none, []).
-spec init(none) -> {ok, state()}.
init(none) ->
ok = log(info, "Starting"),
{AProcs, AMem} = proc_mem(),
TwoMaps = default_spec(mem) * 2,
RProcs =
case AProcs >= 2 of
true -> 2;
false -> 1
end,
RMem =
case AMem >= TwoMaps of
true -> TwoMaps;
false -> default_spec(mem)
end,
{Acc, Keys, Network, MaxCores, MaxMem} =
case read_conf() of
{ok, C} ->
{maps:get(account, C, none),
maps:get(keys, C, []),
maps:get(network, C, <<"mainnet">>),
maps:get(max_cores, C, RProcs),
maps:get(max_mem, C, RMem)};
none ->
{none, [], <<"mainnet">>, RProcs, RMem}
end,
State = #s{network = Network, acc = Acc, keys = Keys, max_cores = MaxCores, max_mem = MaxMem},
{ok, start_gui(State)}.
start_gui(State = #s{acc = none}) ->
Window = gmc_gui:start_link(#{}),
ok = gmc_gui:ask_conf(),
State#s{window = Window};
start_gui(State = #s{acc = AccID}) ->
Window = gmc_gui:start_link(#{}),
ok = gmc_gui:set_account(AccID),
ok =
case lists:member("autostart", zx_daemon:argv()) of
false -> ok;
true -> gmc_gui:start_stop()
end,
State#s{window = Window}.
%%% gen_server Message Handling Callbacks
handle_call(network, _, State = #s{network = Network}) ->
{reply, Network, State};
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
{noreply, State}.
handle_cast(start_stop, State) ->
ok = do_start_stop(State),
{noreply, State};
handle_cast(gajudesk, State) ->
ok = do_gajudesk(),
{noreply, State};
handle_cast(conf, State) ->
NewState = run_gmc_conf(State),
{noreply, NewState};
handle_cast({conf, Info}, State) ->
NewState = do_conf(Info, State),
{noreply, NewState};
handle_cast(stop, State) ->
ok = do_stop(),
ok = log(info, "Received a 'stop' message."),
{noreply, State};
handle_cast(Unexpected, State) ->
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
{noreply, State}.
handle_info({gproc_ps_event, Event, Data}, State) ->
ok = gmc_gui:message({Event, Data}),
{noreply, State};
handle_info({'DOWN', Mon, process, PID, Info}, State = #s{gmc_conf = Mon}) ->
ok = log(info, "gmc_conf ~p closed with ~p", [PID, Info]),
NewState = State#s{gmc_conf = none},
{noreply, NewState};
handle_info(Unexpected, State) ->
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
{noreply, State}.
code_change(_, State, _) ->
{ok, State}.
terminate(Reason, State) ->
ok = log(info, "Reason: ~p,", [Reason]),
ok = persist(State),
case whereis(gd_con) of
undefined ->
zx:stop();
PID ->
ok = log(info, "gd_con found at: ~p", [PID]),
application:stop(gajumine)
end.
do_stop() ->
case is_pid(whereis(gd_con)) of
false -> zx:stop();
true -> application:stop(gajumine)
end.
%%% Doers
do_start_stop(#s{acc = PubKey, keys = Keys, network = Network, max_cores = MaxCores, max_mem = MaxMem}) ->
% smrt guy stuff happens here
{Bits, Eureka} =
case Network of
<<"mainnet">> -> {"29", <<"https://gajumining.com/api/workers/", PubKey/binary>>};
<<"testnet">> -> {"15", <<"https://test.gajumining.com/api/workers/", PubKey/binary>>}
end,
{Fatness, Type} =
case os:type() of
{unix, linux} ->
case Network of
<<"mainnet">> -> {"mean", "avx2"};
<<"testnet">> -> {"mean", "generic"}
end;
{unix, darwin} ->
% Check memory. >7gb gets mean, <7gb gets lean
{"mean", "generic"};
{win32, nt} ->
% Check memory. >7gb gets mean, <7gb gets lean
% Check avx2.
% Both should be provided by the F# start program
case Network of
% <<"mainnet">> -> {"mean", "avx2.exe"};
<<"mainnet">> -> {"mean", "generic.exe"};
<<"testnet">> -> {"mean", "generic.exe"}
end
end,
Miner = filename:join(platform_dir(), unicode:characters_to_binary([Fatness, Bits, "-", Type])),
Count = optimize_count(MaxCores, MaxMem),
Instance = #{<<"executable">> => Miner},
Workers = lists:duplicate(Count, Instance),
Profile =
[{pubkey, PubKey},
{workers, Workers},
{pool_admin_url, Eureka},
{extra_pubkeys, Keys}],
ok = gmc_gui:message({notice, "Starting..."}),
ok = gmc_gui:message({notice, ["Miner: ", Miner]}),
{ok, Apps} = gmhc_app:start(Profile),
Started = io_lib:format("Apps started: ~p", [Apps]),
ok = log(info, Started),
ok = gmc_gui:message({notice, Started}),
Events =
[pool_notification,
connected,
puzzle,
result,
error,
disconnected],
lists:foreach(fun gmhc_events:ensure_subscribed/1, Events).
optimize_count(MaxC, MaxM) ->
MapSize = 3550722201,
MaxCores = max(1, MaxC),
MaxMem = max(MapSize, MaxM),
{Procs, Memory} = proc_mem(),
MeanMaps = min(MaxMem, Memory) div MapSize,
Recommended = min(MaxCores, min(Procs, MeanMaps)),
Notice = fun(F, A) -> gmc_gui:message({notice, io_lib:format(F, A)}) end,
ok = Notice("Physical Processors: ~p", [Procs]),
ok = Notice("Physical Memory: ~p", [Memory]),
ok = Notice("Max Processor Commit: ~p", [MaxCores]),
ok = Notice("Max Memory Commit: ~p", [MaxMem]),
ok = Notice("29-bit Mean Map Space: ~p", [MeanMaps]),
ok = Notice("Workers: ~p", [Recommended]),
Recommended.
platform_dir() ->
Priv = cuckoo_cpu:priv(),
Dir =
case os:type() of
{unix, linux} ->
"linux_x86_64";
{unix, darwin} ->
case string:trim(os:cmd("uname -m")) of
"arm64" ->
"mac_arm64";
"x86_64" ->
"mac_x86_64";
Other ->
ok = log(info, "uname -m: ~p", [Other]),
"mac_x86_64"
end;
{win32, nt} ->
"win_x86_64"
end,
filename:join(Priv, Dir).
proc_mem() ->
case os:type() of
{unix, linux} ->
{processor, Cores} = hd(erlang:system_info(cpu_topology)),
P = length(Cores),
M = s_to_i(mem, string:trim(os:cmd("cat /proc/meminfo | grep MemTotal | awk '{print $2}'"))) * 1024,
{P, M};
{unix, darwin} ->
P = s_to_i(cpu, string:trim(os:cmd("sysctl -n hw.physicalcpu"))),
M = s_to_i(mem, string:trim(os:cmd("sysctl -n hw.memsize"))),
{P, M};
{win32, nt} ->
P = s_to_i(cpu, os:getenv("NUMBER_OF_PROCESSORS")),
M = win_mem(),
{P, M}
end.
s_to_i(Type, "") ->
default_spec(Type);
s_to_i(Type, String) ->
case is_0_9(String) of
true -> list_to_integer(String);
false -> default_spec(Type)
end.
is_0_9("") ->
true;
is_0_9([H | T]) when $0 =< H, H =< $9 ->
is_0_9(T);
is_0_9(_) ->
false.
default_spec(cpu) -> 1;
default_spec(mem) -> 3550722201.
win_mem() ->
Out = os:cmd("powershell -Command \"(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory\""),
s_to_i(mem, lists:last(string:split(string:trim(Out), "\r\n", all))).
do_gajudesk() ->
ok = tell(info, "Running gajudesk"),
PID = spawn(fun run_gajudesk/0),
tell(info, "GajuDesk launched at PID: ~p", [PID]).
run_gajudesk() ->
R = "otpr",
N = "gajudesk",
{ok, V} = zx:latest({R, N}),
{ok, PackageString} = zx_lib:package_string({R, N, V}),
try
case zx:run(PackageString, []) of
ok -> ok;
Error -> tell(error, "gajudesk died with: ~p", [Error])
end
catch
E:R -> tell(error, "gajudesk died with: ~p", [{E, R}])
end.
run_gmc_conf(State = #s{gmc_conf = none, network = Net, acc = Acc, keys = Keys,
max_cores = MProcs, max_mem = MMem,
prefs = Prefs}) ->
{AProcs, AMem} = proc_mem(),
Win = gmc_conf:start_link({Prefs, {Net, Acc, Keys, {AProcs, AMem, MProcs, MMem}}}),
PID = wx_object:get_pid(Win),
Mon = monitor(process, PID),
ok = gmc_conf:to_front(),
State#s{gmc_conf = Mon};
run_gmc_conf(State) ->
ok = gmc_conf:to_front(),
State.
do_conf({Network, AccID, Keys, MaxCores, MaxMem}, State) ->
ok = gmc_gui:set_account(AccID),
NewState =
State#s{network = Network,
acc = AccID,
keys = Keys,
max_cores = MaxCores,
max_mem = MaxMem},
ok = persist(NewState),
NewState.
%%% Utils
%% Paths and Names
persist(#s{network = Network, acc = Acc, keys = Keys, max_cores = MaxCores, max_mem = MaxMem, prefs = Prefs}) ->
Update = #{network => Network, account => Acc, keys => Keys, max_cores => MaxCores, max_mem => MaxMem},
Conf = maps:to_list(maps:merge(Prefs, Update)),
Path = gmc_prefs_path(),
ok = filelib:ensure_dir(Path),
zx_lib:write_terms(Path, Conf).
read_conf() ->
Path = gmc_prefs_path(),
case file:consult(Path) of
{ok, Conf} ->
{ok, maps:from_list(Conf)};
{error, enoent} ->
none;
{error, Other} ->
ok = log(info, "Path ~p could not be read. Failed with ~p. Continuing.", [Path, Other]),
none
end.
gmc_prefs_path() ->
filename:join(zx_lib:path(etc, "qpq", "gajumine"), "prefs.eterms").