%%% @doc %%% GajuMine Controller %%% @end -module(gmc_con). -vsn("0.3.1"). -author("Craig Everett "). -copyright("QPQ AG "). -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").