%%% @doc %%% GajuMine Controller %%% @end -module(gmc_con). -vsn("0.2.0"). -author("Craig Everett "). -copyright("Craig Everett "). -license("GPL-3.0-or-later"). -behavior(gen_server). -export([start_stop/0, gajudesk/0]). -export([conf/0, conf/1]). -export([bin_dir/0]). -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">> exec_dir = platform_dir() :: file:filename(), 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 bin_dir() -> file:filename(). bin_dir() -> gen_server:call(?MODULE, bin_dir). -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"), _ = process_flag(sensitive, true), {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, 2), maps:get(max_mem, C, 3550722201)}; none -> {none, [], <<"mainnet">>, 2, 3550722201} end, State = #s{network = Network, acc = Acc, keys = Keys, max_cores = MaxCores, max_mem = MaxMem}, {ok, start_gui(State)}. platform_dir() -> #{deps := Deps} = zx_daemon:meta(), AppID = lists:keyfind("cuckoo_cpu", 2, Deps), Priv = filename:join(zx_lib:ppath(lib, AppID), "priv"), Dir = case os:type() of {unix, linux} -> "linux_x86_64"; {unix, darwin} -> case erlang:system_info(system_architecture) of "aarch64-" ++ _ -> "mac_m2"; Arch -> ok = log(info, "system_architecture: ~p", [Arch]), "mac_x86_64" end; {win32, nt} -> "win_x86_64" end, filename:join(Priv, Dir). start_gui(State = #s{acc = none}) -> Window = gmc_gui:start_link(#{}), ok = gmc_gui:ask_conf(), State#s{window = Window}; start_gui(State) -> Window = gmc_gui:start_link(#{}), State#s{window = Window}. %%% gen_server Message Handling Callbacks handle_call(network, _, State = #s{network = Network}) -> {reply, Network, State}; handle_call(bin_dir, _, State = #s{exec_dir = BinDir}) -> {reply, BinDir, 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, network = Network, 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"}; <<"testnet">> -> {"mean", "generic.exe"} end end, Miner = unicode:characters_to_binary([Fatness, Bits, "-", Type]), Count = optimize_count(MaxMem), Instance = #{<<"executable">> => Miner}, Workers = lists:duplicate(Count, Instance), Profile = [{pubkey, PubKey}, {workers, Workers}, {pool_admin_url, Eureka}], 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(MaxMem) -> {Procs, Memory} = proc_mem(), MeanMaps = min(MaxMem, Memory) div 3550722201, Recommended = max(min(Procs, MeanMaps) - 1, 1), 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("29-bit Mean Map Space: ~p", [MeanMaps]), ok = Notice("Workers: ~p", [Recommended]), Recommended. proc_mem() -> case os:type() of {unix, linux} -> {processor, Cores} = hd(erlang:system_info(cpu_topology)), P = length(Cores), M = list_to_integer(string:trim(os:cmd("cat /proc/meminfo | grep MemTotal | awk '{print $2}'"))) * 1024, {P, M}; {unix, darwin} -> P = list_to_integer(string:trim(os:cmd("sysctl -n hw.physicalcpu"))), M = list_to_integer(string:trim(os:cmd("sysctl -n hw.memsize"))), {P, M}; {win32, nt} -> P = list_to_integer(os:getenv("NUMBER_OF_PROCESSORS")), M = list_to_integer(string:strip(lists:nth(2, string:split(os:cmd("wmic computersystem get TotalPhysicalMemory"), "\r\r\n", all)))), {P, M} end. 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) -> State#s{network = Network, acc = AccID, keys = Keys, max_cores = MaxCores, max_mem = MaxMem}. %%% 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").