Compare commits
10 Commits
d99fefcd15
...
51c081fb55
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51c081fb55 | ||
|
|
2cfa90beb1 | ||
|
|
f73756c15a | ||
|
|
d26cb75331 | ||
|
|
46aacfb621 | ||
|
|
6bfd19e027 | ||
|
|
c270e18ed0 | ||
|
|
46b93158db | ||
|
|
6ba256f016 | ||
|
|
7299dbc9f1 |
19
LICENSE
Normal file
19
LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2025-2026 QPQ AG
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 219 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
BIN
etc/observer.png
BIN
etc/observer.png
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@ -1,51 +0,0 @@
|
||||
-module(pingpong).
|
||||
-export([main/0]).
|
||||
|
||||
% this runs two processes and has them talk to each other
|
||||
% output:
|
||||
%
|
||||
% 2> pingpong:main().
|
||||
% <0.96.0> received {pong, <0.97.0>, 10}
|
||||
% <0.97.0> received {ping, <0.96.0>, 9}
|
||||
% <0.96.0> received {pong, <0.97.0>, 8}
|
||||
% <0.97.0> received {ping, <0.96.0>, 7}
|
||||
% <0.96.0> received {pong, <0.97.0>, 6}
|
||||
% <0.97.0> received {ping, <0.96.0>, 5}
|
||||
% <0.96.0> received {pong, <0.97.0>, 4}
|
||||
% <0.97.0> received {ping, <0.96.0>, 3}
|
||||
% <0.96.0> received {pong, <0.97.0>, 2}
|
||||
% <0.97.0> received {ping, <0.96.0>, 1}
|
||||
% <0.96.0> received {pong, <0.97.0>, 0}
|
||||
|
||||
|
||||
main() ->
|
||||
PingerPID = spawn(fun pinger/0),
|
||||
PongerPID = spawn(fun ponger/0),
|
||||
PingerPID ! {pong, PongerPID, 10}.
|
||||
|
||||
pinger() ->
|
||||
receive
|
||||
{pong, PID, N} ->
|
||||
io:format("~p received {pong, ~p, ~p}~n", [self(), PID, N]),
|
||||
% ignore once we get to 0 or lower
|
||||
case N > 0 of
|
||||
true -> PID ! {ping, self(), N-1};
|
||||
false -> ok
|
||||
end,
|
||||
% once we're done, go back to the top
|
||||
pinger()
|
||||
end.
|
||||
|
||||
|
||||
ponger() ->
|
||||
receive
|
||||
{ping, PID, N} ->
|
||||
io:format("~p received {ping, ~p, ~p}~n", [self(), PID, N]),
|
||||
% ignore once we get to 0 or lower
|
||||
case N > 0 of
|
||||
true -> PID ! {pong, self(), N-1};
|
||||
false -> ok
|
||||
end,
|
||||
% once we're done, go back to the top
|
||||
ponger()
|
||||
end.
|
||||
BIN
etc/v001-end.png
BIN
etc/v001-end.png
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 219 KiB |
1
gex_httpd/.gitignore
vendored
1
gex_httpd/.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
priv/keypair.eterms
|
||||
.eunit
|
||||
deps
|
||||
*.o
|
||||
|
||||
26
gex_httpd/README.md
Normal file
26
gex_httpd/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
`gex_httpd`: Gajumaru Exchange HTTP Daemon
|
||||
=====================================================================
|
||||
|
||||
GOALS
|
||||
====================================================================
|
||||
|
||||
|
||||
GOAL STACK
|
||||
--------------------------------------------------------------------
|
||||
|
||||
- use `gh_sfc` in `gh_client`
|
||||
- don't spam filesystem for 404/500
|
||||
|
||||
|
||||
TODONE
|
||||
--------------------------------------------------------------------
|
||||
|
||||
- ~~replace `io:format` calls with zx log~~
|
||||
- ~~write out call paths for `gh_sfc`~~
|
||||
|
||||
|
||||
GOAL QUEUE
|
||||
--------------------------------------------------------------------
|
||||
|
||||
- mit license
|
||||
- copyright/author bullshit in each module
|
||||
@ -5,5 +5,6 @@
|
||||
{applications,[stdlib,kernel]},
|
||||
{vsn,"0.1.0"},
|
||||
{modules,[gex_httpd,gh_client,gh_client_man,gh_client_sup,
|
||||
gh_clients,gh_sup]},
|
||||
gh_clients,gh_ct,gh_sfc,gh_sfc_cache,gh_sfc_entry,
|
||||
gh_sup]},
|
||||
{mod,{gex_httpd,[]}}]}.
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>QHL: 404</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>404 Not Found</h1>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>QHL: 500</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>500 Internal Server Error</h1>
|
||||
</body>
|
||||
</html>
|
||||
16
gex_httpd/priv/ct/hello.aes
Normal file
16
gex_httpd/priv/ct/hello.aes
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Hello world contract in sophia
|
||||
*
|
||||
* Copyright (C) 2025, QPQ AG
|
||||
*/
|
||||
|
||||
@compiler == 9.0.0
|
||||
|
||||
contract Hello =
|
||||
type state = unit
|
||||
|
||||
entrypoint init(): state =
|
||||
()
|
||||
|
||||
entrypoint hello(): string =
|
||||
"hello"
|
||||
@ -3,20 +3,26 @@
|
||||
%%% @end
|
||||
|
||||
-module(gex_httpd).
|
||||
-vsn("0.1.0").
|
||||
-behavior(application).
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
%% for our edification
|
||||
-export([listen/1, ignore/0]).
|
||||
-export([start/0]).
|
||||
|
||||
%% erlang expects us to export these functions
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
|
||||
%------------------------------------------------------
|
||||
% API
|
||||
%------------------------------------------------------
|
||||
|
||||
|
||||
-spec listen(PortNum) -> Result
|
||||
when PortNum :: inet:port_num(),
|
||||
@ -38,27 +44,6 @@ ignore() ->
|
||||
gh_client_man:ignore().
|
||||
|
||||
|
||||
-spec start() -> ok.
|
||||
%% @doc
|
||||
%% Start the server in an "ignore" state.
|
||||
|
||||
start() ->
|
||||
ok = application:ensure_started(sasl),
|
||||
ok = application:start(gex_httpd),
|
||||
io:format("Starting...").
|
||||
|
||||
|
||||
%-spec start(PortNum) -> ok
|
||||
% when PortNum :: inet:port_number().
|
||||
%%% @doc
|
||||
%%% Start the server and begin listening immediately. Slightly more convenient when
|
||||
%%% playing around in the shell.
|
||||
%
|
||||
%start(PortNum) ->
|
||||
% ok = start(),
|
||||
% ok = gh_client_man:listen(PortNum),
|
||||
% io:format("Startup complete, listening on ~w~n", [PortNum]).
|
||||
%
|
||||
|
||||
-spec start(normal, term()) -> {ok, pid()}.
|
||||
%% @private
|
||||
@ -68,27 +53,25 @@ start() ->
|
||||
|
||||
start(normal, _Args) ->
|
||||
Result = gh_sup:start_link(),
|
||||
% auto-listen to port 8000
|
||||
ok = hz(),
|
||||
% auto-listen to port 8000
|
||||
ok = listen(8000),
|
||||
Result.
|
||||
|
||||
|
||||
hz() ->
|
||||
ok = application:ensure_started(hakuzaru),
|
||||
ok = hz:chain_nodes([testnet_node()]),
|
||||
ok = zx:tell("hz status: ~tp", [hz:status()]),
|
||||
%TestnetIP = {84, 46, 242, 9},
|
||||
% fuck bulgaria
|
||||
% TestnetIP = "groot.testnet.gajumaru.io",
|
||||
% TestnetPort = 3013,
|
||||
% japan good
|
||||
TestnetIP = "tsuriai.jp",
|
||||
TestnetPort = 4013,
|
||||
TestnetNode = {TestnetIP, TestnetPort},
|
||||
ok = hz:chain_nodes([TestnetNode]),
|
||||
ok = tell("hz status: ~tp", [hz:status()]),
|
||||
ok.
|
||||
|
||||
testnet_ip() ->
|
||||
{84, 46, 242, 9}.
|
||||
|
||||
testnet_port() ->
|
||||
3013.
|
||||
|
||||
testnet_node() ->
|
||||
{testnet_ip(), testnet_port()}.
|
||||
|
||||
|
||||
-spec stop(term()) -> ok.
|
||||
%% @private
|
||||
|
||||
@ -14,9 +14,11 @@
|
||||
%%% @end
|
||||
|
||||
-module(gh_client).
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
|
||||
-export([start/1]).
|
||||
@ -24,6 +26,7 @@
|
||||
-export([system_continue/3, system_terminate/4,
|
||||
system_get_state/1, system_replace_state/2]).
|
||||
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
-include("http.hrl").
|
||||
|
||||
|
||||
@ -85,7 +88,7 @@ start_link(ListenSocket) ->
|
||||
%% call to listen/3.
|
||||
|
||||
init(Parent, ListenSocket) ->
|
||||
ok = io:format("~p Listening.~n", [self()]),
|
||||
ok = tell("~p Listening.~n", [self()]),
|
||||
Debug = sys:debug_options([]),
|
||||
ok = proc_lib:init_ack(Parent, {ok, self()}),
|
||||
listen(Parent, Debug, ListenSocket).
|
||||
@ -109,12 +112,12 @@ listen(Parent, Debug, ListenSocket) ->
|
||||
{ok, Socket} ->
|
||||
{ok, _} = start(ListenSocket),
|
||||
{ok, Peer} = inet:peername(Socket),
|
||||
ok = io:format("~p Connection accepted from: ~p~n", [self(), Peer]),
|
||||
ok = tell("~p Connection accepted from: ~p~n", [self(), Peer]),
|
||||
ok = gh_client_man:enroll(),
|
||||
State = #s{socket = Socket},
|
||||
loop(Parent, Debug, State);
|
||||
{error, closed} ->
|
||||
ok = io:format("~p Retiring: Listen socket closed.~n", [self()]),
|
||||
ok = log("~p Retiring: Listen socket closed.~n", [self()]),
|
||||
exit(normal)
|
||||
end.
|
||||
|
||||
@ -131,7 +134,6 @@ loop(Parent, Debug, State = #s{socket = Socket, received = Received}) ->
|
||||
ok = inet:setopts(Socket, [{active, once}]),
|
||||
receive
|
||||
{tcp, Socket, Message} ->
|
||||
ok = io:format("~p received: ~tp~n", [self(), Message]),
|
||||
%% Received exists because web browsers usually use the same
|
||||
%% acceptor socket for sequential requests
|
||||
%%
|
||||
@ -151,21 +153,27 @@ loop(Parent, Debug, State = #s{socket = Socket, received = Received}) ->
|
||||
%% see: https://git.qpq.swiss/QPQ-AG/QHL/pulls/1
|
||||
case qhl:parse(Socket, Message2) of
|
||||
{ok, Request, NewReceived} ->
|
||||
ok = handle_request(Socket, Request),
|
||||
try
|
||||
ok = handle_request(Socket, Request)
|
||||
catch
|
||||
X:Y:Z ->
|
||||
tell(error, "~tp ~tp: CRASH: ~tp:~tp:~tp, returning 500", [?MODULE, self(), X, Y, Z]),
|
||||
http_err(Socket, 500)
|
||||
end,
|
||||
NewState = State#s{received = NewReceived},
|
||||
loop(Parent, Debug, NewState);
|
||||
{error, Reason} ->
|
||||
io:format("~p error: ~tp~n", [self(), Reason]),
|
||||
tell(warning, "~p ~p: http parse error: ~tp~n", [?MODULE, self(), Reason]),
|
||||
ok = http_err(Socket, 500),
|
||||
exit(normal)
|
||||
end;
|
||||
{tcp_closed, Socket} ->
|
||||
ok = io:format("~p Socket closed, retiring.~n", [self()]),
|
||||
ok = log(warning, "~p ~p: Socket closed, retiring.~n", [?MODULE, self()]),
|
||||
exit(normal);
|
||||
{system, From, Request} ->
|
||||
sys:handle_system_msg(Request, From, Parent, ?MODULE, Debug, State);
|
||||
Unexpected ->
|
||||
ok = io:format("~p Unexpected message: ~tp", [self(), Unexpected]),
|
||||
ok = tell("~p ~p: Unexpected message: ~tp", [?MODULE, self(), Unexpected]),
|
||||
loop(Parent, Debug, State)
|
||||
end.
|
||||
|
||||
@ -230,26 +238,54 @@ system_replace_state(StateFun, State) ->
|
||||
|
||||
%% ref: https://git.qpq.swiss/QPQ-AG/QHL/src/commit/7f77f9e3b19f58006df88a2a601e85835d300c37/include/http.hrl
|
||||
|
||||
handle_request(Socket, #request{method = get, path = <<"/">>}) ->
|
||||
IndexHtmlPath = filename:join([zx:get_home(), "priv", "index.html"]),
|
||||
case file:read_file(IndexHtmlPath) of
|
||||
{ok, ResponseBody} ->
|
||||
%% see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Messages#http_responses
|
||||
Headers = [{"content-type", "text/html"}],
|
||||
Response = #response{headers = Headers,
|
||||
body = ResponseBody},
|
||||
respond(Socket, Response);
|
||||
Error ->
|
||||
io:format("~p error: ~p~n", [self(), Error]),
|
||||
http_err(Socket, 500)
|
||||
handle_request(Socket, #request{method = get, path = Path}) ->
|
||||
% future-proofing for hardcoded paths
|
||||
case Path of
|
||||
_ -> handle_static(Socket, Path)
|
||||
end;
|
||||
handle_request(Socket, _) ->
|
||||
http_err(Socket, 404).
|
||||
|
||||
|
||||
-spec handle_static(Socket, Path) -> ok
|
||||
when Socket :: gen_tcp:socket(),
|
||||
Path :: binary().
|
||||
|
||||
handle_static(Socket, <<"/">>) ->
|
||||
handle_static(Socket, <<"/index.html">>);
|
||||
handle_static(Socket, Path) ->
|
||||
case gh_sfc:query(Path) of
|
||||
{found, Entry} -> handle_entry(Socket, Entry);
|
||||
not_found -> http_err(Socket, 404)
|
||||
end.
|
||||
|
||||
|
||||
-spec handle_entry(Socket, Entry) -> ok
|
||||
when Socket :: gen_tcp:socket(),
|
||||
Entry :: gh_sfc_entry:entry().
|
||||
|
||||
handle_entry(Socket, Entry) ->
|
||||
% -type encoding() :: none | gzip.
|
||||
% -record(e, {fs_path :: file:filename(),
|
||||
% last_modified :: file:date_time(),
|
||||
% mime_type :: string(),
|
||||
% encoding :: encoding(),
|
||||
% contents :: binary()}).
|
||||
Encoding = gh_sfc_entry:encoding(Entry) ,
|
||||
MimeType = gh_sfc_entry:mime_type(Entry),
|
||||
Contents = gh_sfc_entry:contents(Entry),
|
||||
Headers0 =
|
||||
case Encoding of
|
||||
gzip -> [{"content-encoding", "gzip"}];
|
||||
none -> []
|
||||
end,
|
||||
Headers1 = [{"content-type", MimeType} | Headers0],
|
||||
Response = #response{headers = Headers1,
|
||||
body = Contents},
|
||||
respond(Socket, Response).
|
||||
|
||||
http_err(Socket, 404) ->
|
||||
HtmlPath = filename:join([zx:get_home(), "priv", "404.html"]),
|
||||
{ok, ResponseBody} = file:read_file(HtmlPath),
|
||||
ResponseBody = bdy_404(),
|
||||
Headers = [{"content-type", "text/html"}],
|
||||
Response = #response{headers = Headers,
|
||||
code = 404,
|
||||
@ -257,8 +293,7 @@ http_err(Socket, 404) ->
|
||||
respond(Socket, Response);
|
||||
% default error is 500
|
||||
http_err(Socket, _) ->
|
||||
HtmlPath = filename:join([zx:get_home(), "priv", "500.html"]),
|
||||
{ok, ResponseBody} = file:read_file(HtmlPath),
|
||||
ResponseBody = bdy_500(),
|
||||
Headers = [{"content-type", "text/html"}],
|
||||
Response = #response{headers = Headers,
|
||||
code = 500,
|
||||
@ -266,11 +301,39 @@ http_err(Socket, _) ->
|
||||
respond(Socket, Response).
|
||||
|
||||
|
||||
bdy_404() ->
|
||||
["<!DOCTYPE html>"
|
||||
"<html lang=\"en\">"
|
||||
"<head>"
|
||||
"<meta charset=\"UTF-8\">"
|
||||
"<title>QHL: 404</title>"
|
||||
"</head>"
|
||||
"<body>"
|
||||
"<h1>404 Not Found</h1>"
|
||||
"</body>"
|
||||
"</html>"].
|
||||
|
||||
|
||||
bdy_500() ->
|
||||
["<!DOCTYPE html>"
|
||||
"<html lang=\"en\">"
|
||||
"<head>"
|
||||
"<meta charset=\"UTF-8\">"
|
||||
"<title>QHL: 500 Internal Server Error</title>"
|
||||
"</head>"
|
||||
"<body>"
|
||||
"<h1>500 Internal Server Error</h1>"
|
||||
"</body>"
|
||||
"</html>"].
|
||||
|
||||
|
||||
respond(Socket, R = #response{code = Code, headers = Headers, body = Body}) ->
|
||||
Slogan = slogan(Code),
|
||||
ContentLength = byte_size(Body),
|
||||
DefaultHeaders = [{"date", qhl:ridiculous_web_date()},
|
||||
{"content-length", integer_to_list(ContentLength)}],
|
||||
BodyBytes = iolist_to_binary(Body),
|
||||
ContentLength = byte_size(BodyBytes),
|
||||
DefaultHeaders = [{"Server", "gex_httpd 0.1.0"},
|
||||
{"Date", qhl:ridiculous_web_date()},
|
||||
{"Content-Length", integer_to_list(ContentLength)}],
|
||||
Headers2 = merge_headers(DefaultHeaders, Headers),
|
||||
really_respond(Socket, R#response{slogan = Slogan,
|
||||
headers = Headers2}).
|
||||
|
||||
@ -10,10 +10,12 @@
|
||||
%%% @end
|
||||
|
||||
-module(gh_client_man).
|
||||
-vsn("0.1.0").
|
||||
-behavior(gen_server).
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
|
||||
-export([listen/1, ignore/0]).
|
||||
@ -23,6 +25,8 @@
|
||||
code_change/3, terminate/2]).
|
||||
|
||||
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
|
||||
%%% Type and Record Definitions
|
||||
|
||||
|
||||
@ -93,7 +97,7 @@ start_link() ->
|
||||
%% preparatory work necessary for proper function.
|
||||
|
||||
init(none) ->
|
||||
ok = io:format("Starting.~n"),
|
||||
ok = tell("~p ~p: Starting.~n", [?MODULE, self()]),
|
||||
State = #s{},
|
||||
{ok, State}.
|
||||
|
||||
@ -119,7 +123,7 @@ handle_call({listen, PortNum}, _, State) ->
|
||||
{Response, NewState} = do_listen(PortNum, State),
|
||||
{reply, Response, NewState};
|
||||
handle_call(Unexpected, From, State) ->
|
||||
ok = io:format("~p Unexpected call from ~tp: ~tp~n", [self(), From, Unexpected]),
|
||||
ok = tell("~p ~p Unexpected call from ~tp: ~tp~n", [?MODULE, self(), From, Unexpected]),
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
@ -138,7 +142,7 @@ handle_cast(ignore, State) ->
|
||||
NewState = do_ignore(State),
|
||||
{noreply, NewState};
|
||||
handle_cast(Unexpected, State) ->
|
||||
ok = io:format("~p Unexpected cast: ~tp~n", [self(), Unexpected]),
|
||||
ok = tell("~p Unexpected cast: ~tp~n", [self(), Unexpected]),
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
@ -154,7 +158,7 @@ handle_info({'DOWN', Mon, process, Pid, Reason}, State) ->
|
||||
NewState = handle_down(Mon, Pid, Reason, State),
|
||||
{noreply, NewState};
|
||||
handle_info(Unexpected, State) ->
|
||||
ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
||||
ok = tell("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
@ -206,7 +210,6 @@ terminate(_, _) ->
|
||||
do_listen(PortNum, State = #s{port_num = none}) ->
|
||||
SocketOptions =
|
||||
[inet6,
|
||||
{packet, line},
|
||||
{active, once},
|
||||
{mode, binary},
|
||||
{keepalive, true},
|
||||
@ -215,7 +218,7 @@ do_listen(PortNum, State = #s{port_num = none}) ->
|
||||
{ok, _} = gh_client:start(Listener),
|
||||
{ok, State#s{port_num = PortNum, listener = Listener}};
|
||||
do_listen(_, State = #s{port_num = PortNum}) ->
|
||||
ok = io:format("~p Already listening on ~p~n", [self(), PortNum]),
|
||||
ok = tell("~p ~p: Already listening on ~p~n", [?MODULE, self(), PortNum]),
|
||||
{{error, {listening, PortNum}}, State}.
|
||||
|
||||
|
||||
@ -241,7 +244,7 @@ do_enroll(Pid, State = #s{clients = Clients}) ->
|
||||
case lists:member(Pid, Clients) of
|
||||
false ->
|
||||
Mon = monitor(process, Pid),
|
||||
ok = io:format("Monitoring ~tp @ ~tp~n", [Pid, Mon]),
|
||||
ok = tell("Monitoring ~tp @ ~tp~n", [Pid, Mon]),
|
||||
State#s{clients = [Pid | Clients]};
|
||||
true ->
|
||||
State
|
||||
@ -268,6 +271,6 @@ handle_down(Mon, Pid, Reason, State = #s{clients = Clients}) ->
|
||||
State#s{clients = NewClients};
|
||||
false ->
|
||||
Unexpected = {'DOWN', Mon, process, Pid, Reason},
|
||||
ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
||||
ok = tell("~p Unexpected info: ~tp~n", [self(), Unexpected]),
|
||||
State
|
||||
end.
|
||||
|
||||
@ -14,12 +14,12 @@
|
||||
%%% @end
|
||||
|
||||
-module(gh_client_sup).
|
||||
-vsn("0.1.0").
|
||||
-behaviour(supervisor).
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
|
||||
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
-export([start_acceptor/1]).
|
||||
-export([start_link/0]).
|
||||
|
||||
@ -9,10 +9,12 @@
|
||||
%%% @end
|
||||
|
||||
-module(gh_clients).
|
||||
-vsn("0.1.0").
|
||||
-behavior(supervisor).
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
168
gex_httpd/src/gh_ct.erl
Normal file
168
gex_httpd/src/gh_ct.erl
Normal file
@ -0,0 +1,168 @@
|
||||
% @doc miscellaneous contract functions
|
||||
%
|
||||
% mostly wrappers for ec_utils and hakuzaru
|
||||
-module(gh_ct).
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
-export_type([
|
||||
keypair/0
|
||||
]).
|
||||
|
||||
-export([
|
||||
deploy/2,
|
||||
get_pubkey_akstr/0, get_keypair/0,
|
||||
keypair_file/0,
|
||||
read_keypair_from_file/1, write_keypair_to_file/2, fmt_keypair/1,
|
||||
fmt_pubkey_api/1,
|
||||
gen_keypair/0
|
||||
]).
|
||||
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
|
||||
%------------------------------------------------------
|
||||
% API: types
|
||||
%------------------------------------------------------
|
||||
|
||||
|
||||
-type keypair() :: #{public := binary(),
|
||||
secret := binary()}.
|
||||
|
||||
|
||||
%------------------------------------------------------
|
||||
% API: functions
|
||||
%------------------------------------------------------
|
||||
|
||||
-spec deploy(ContractSrcPath, InitArgs) -> Result
|
||||
when ContractSrcPath :: string(),
|
||||
InitArgs :: term(),
|
||||
Result :: {ok, term()}
|
||||
| {error, term()}. %% FIXME
|
||||
|
||||
deploy(ContractSrcPath, InitArgs) ->
|
||||
CreatorId = get_pubkey_akstr(),
|
||||
case hz:contract_create(CreatorId, ContractSrcPath, InitArgs) of
|
||||
{ok, ContractCreateTx} ->
|
||||
push(ContractCreateTx);
|
||||
Error ->
|
||||
tell(error, "gh_ct:deploy(~tp, ~tp) error: ~tp", [ContractSrcPath, InitArgs, Error]),
|
||||
Error
|
||||
end.
|
||||
|
||||
push(ContractCreateTx) ->
|
||||
#{secret := SecretKey} = get_keypair(),
|
||||
SignedTx = hz:sign_tx(ContractCreateTx, SecretKey),
|
||||
tell(info, "pushing signed tx: ~tp", [SignedTx]),
|
||||
hz:post_tx(SignedTx).
|
||||
|
||||
|
||||
|
||||
-spec get_pubkey_akstr() -> string().
|
||||
% @doc
|
||||
% get our pubkey as an ak_... string
|
||||
|
||||
get_pubkey_akstr() ->
|
||||
#{public := PK} = get_keypair(),
|
||||
unicode:characters_to_list(fmt_pubkey_api(PK)).
|
||||
|
||||
|
||||
-spec get_keypair() -> keypair().
|
||||
% @doc
|
||||
% if can read keypair from `keypair_file()`, do so
|
||||
% otherwise generate one
|
||||
%
|
||||
% prints warnings if IO ops fail
|
||||
|
||||
get_keypair() ->
|
||||
case read_keypair_from_file(keypair_file()) of
|
||||
{ok, KP} ->
|
||||
KP;
|
||||
% probably file
|
||||
ReadError ->
|
||||
tell(warning, "gh_ct:get_keypair(): read error: ~tp", [ReadError]),
|
||||
KP = gen_keypair(),
|
||||
% try writing to file
|
||||
%tell(info, "gh_ct:get_keypair(): attempting to write keypair to file...", []),
|
||||
%case write_keypair_to_file(keypair_file(), KP) of
|
||||
% ok -> tell(info, "gh_ct:get_keypair(): write successful!", []);
|
||||
% Error -> tell(warning, "gh_ct:get_keypair(): write error: ~tp", [Error])
|
||||
%end,
|
||||
KP
|
||||
end.
|
||||
|
||||
|
||||
-spec keypair_file() -> string().
|
||||
% @doc
|
||||
% normal file where operating keypair is stored
|
||||
|
||||
keypair_file() ->
|
||||
filename:join([zx:get_home(), "priv", "keypair.eterms"]).
|
||||
|
||||
|
||||
|
||||
-spec read_keypair_from_file(FilePath) -> Result
|
||||
when FilePath :: string(),
|
||||
Result :: {ok, keypair()}
|
||||
| {error, Reason :: term()}.
|
||||
% @doc
|
||||
% try to read keypair from file in `file:consult/1` format.
|
||||
|
||||
read_keypair_from_file(FilePath) ->
|
||||
case file:consult(FilePath) of
|
||||
{ok, [{public, PK}, {secret, SK}]} ->
|
||||
{ok, #{public => PK, secret => SK}};
|
||||
{ok, [{secret, SK}, {public, PK}]} ->
|
||||
{ok, #{public => PK, secret => SK}};
|
||||
{ok, Bad} ->
|
||||
tell(warning, "read malformed keypair from file ~tp: ~tp", [FilePath, Bad]),
|
||||
{error, bad_keypair};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
|
||||
|
||||
-spec write_keypair_to_file(FilePath, Keypair) -> Result
|
||||
when FilePath :: string(),
|
||||
Keypair :: keypair(),
|
||||
Result :: ok
|
||||
| {error, Reason :: term()}.
|
||||
% @doc
|
||||
% Write keypair to file as
|
||||
%
|
||||
% ```
|
||||
% {public, <<...>>}.
|
||||
% {secret, <<..>>}.
|
||||
% ```
|
||||
|
||||
write_keypair_to_file(FP, KP) ->
|
||||
file:write_file(FP, fmt_keypair(KP)).
|
||||
|
||||
|
||||
|
||||
-spec fmt_pubkey_api(binary()) -> binary().
|
||||
|
||||
fmt_pubkey_api(Bin) ->
|
||||
gmser_api_encoder:encode(account_pubkey, Bin).
|
||||
|
||||
|
||||
-spec fmt_keypair(keypair()) -> iolist().
|
||||
% @doc
|
||||
% format keypair in `file:consult/1` format
|
||||
|
||||
fmt_keypair(#{public := PK, secret := SK}) ->
|
||||
io_lib:format("{public, ~tp}.~n"
|
||||
"{secret, ~tp}.~n",
|
||||
[PK, SK]).
|
||||
|
||||
|
||||
|
||||
-spec gen_keypair() -> keypair().
|
||||
% @doc
|
||||
% Generate a keypair
|
||||
|
||||
gen_keypair() ->
|
||||
ecu_eddsa:sign_keypair().
|
||||
154
gex_httpd/src/gh_sfc.erl
Normal file
154
gex_httpd/src/gh_sfc.erl
Normal file
@ -0,0 +1,154 @@
|
||||
% @doc static file cache
|
||||
%
|
||||
% polls priv/static for sheeeit
|
||||
%
|
||||
% Adapted from FEWD: https://git.qpq.swiss/pharpend/fewd/src/commit/9adbf67ebde14c7c1d8de70ec9b241e6d4ee6f45/src/fd_httpd_sfc.erl
|
||||
-module(gh_sfc).
|
||||
-behavior(gen_server).
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
-export_type([
|
||||
entry/0,
|
||||
maybe_entry/0
|
||||
]).
|
||||
|
||||
%% caller context: actual api
|
||||
-export([
|
||||
base_path/0,
|
||||
renew/0,
|
||||
query/1
|
||||
]).
|
||||
|
||||
%% caller context: startup
|
||||
-export([start_link/0 ]).
|
||||
%% gen_server callbacks (process context)
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
||||
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
|
||||
|
||||
-type cache() :: gh_sfc_cache:cache().
|
||||
-type entry() :: gh_sfc_entry:entry().
|
||||
-type maybe_entry() :: {found, entry()} | not_found.
|
||||
|
||||
|
||||
-record(s, {base_path = base_path() :: file:filename(),
|
||||
cache = gh_sfc_cache:new(base_path()) :: cache(),
|
||||
auto_renew_ms = 1_000 :: pos_integer()}).
|
||||
|
||||
-type state() :: #s{}.
|
||||
|
||||
|
||||
%%------------------------------------------------------------------
|
||||
%% API (ACTUAL API / CALLER CONTEXT)
|
||||
%%------------------------------------------------------------------
|
||||
|
||||
-spec base_path() -> file:filename().
|
||||
|
||||
base_path() ->
|
||||
filename:join([zx:get_home(), "priv", "static"]).
|
||||
|
||||
|
||||
|
||||
-spec renew() -> ok.
|
||||
|
||||
renew() ->
|
||||
gen_server:cast(?MODULE, renew).
|
||||
|
||||
|
||||
|
||||
-spec query(HttpPath) -> MaybeEntry
|
||||
when HttpPath :: binary(),
|
||||
MaybeEntry :: maybe_entry().
|
||||
|
||||
query(Path) ->
|
||||
gen_server:call(?MODULE, {query, Path}).
|
||||
|
||||
|
||||
|
||||
%%------------------------------------------------------------------
|
||||
%% API (STARTUP / CALLER CONTEXT)
|
||||
%%------------------------------------------------------------------
|
||||
|
||||
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, none, []).
|
||||
|
||||
|
||||
|
||||
%%------------------------------------------------------------------
|
||||
%% API (GEN_SERVER CALLBACKS / PROCESS CONTEXT)
|
||||
%%------------------------------------------------------------------
|
||||
|
||||
-spec init(Args) -> {ok, InitState}
|
||||
when Args :: none,
|
||||
InitState :: state().
|
||||
|
||||
init(none) ->
|
||||
ok = tell("starting gh_sfc"),
|
||||
InitState = #s{},
|
||||
#s{auto_renew_ms = AutoRenewInterval} = InitState,
|
||||
erlang:send_after(AutoRenewInterval, self(), auto_renew),
|
||||
{ok, InitState}.
|
||||
|
||||
|
||||
|
||||
-spec handle_call(Request, From, State) -> MaybeReply
|
||||
when Request :: term(),
|
||||
From :: {pid(), Tag :: term()},
|
||||
State :: state(),
|
||||
MaybeReply :: {reply, Reply, NewState}
|
||||
| {noreply, NewState},
|
||||
Reply :: term(),
|
||||
NewState :: State.
|
||||
|
||||
handle_call({query, Path}, _, State = #s{cache = Cache}) ->
|
||||
Reply = gh_sfc_cache:query(Path, Cache),
|
||||
{reply, Reply, State};
|
||||
handle_call(Unexpected, From, State) ->
|
||||
ok = log(warning, "~p ~p: unexpected call from ~p: ~p", [?MODULE, self(), From, Unexpected]),
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
|
||||
-spec handle_cast(Request, State) -> {noreply, NewState}
|
||||
when Request :: term(),
|
||||
State :: state(),
|
||||
NewState :: State.
|
||||
|
||||
handle_cast(renew, State) ->
|
||||
NewState = i_renew(State),
|
||||
{noreply, NewState};
|
||||
handle_cast(Unexpected, State) ->
|
||||
ok = log(warning, "~p ~p: unexpected cast: ~p", [?MODULE, self(), Unexpected]),
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
|
||||
-spec handle_info(Info, State) -> {noreply, NewState}
|
||||
when Info :: term(),
|
||||
State :: state(),
|
||||
NewState :: State.
|
||||
|
||||
handle_info(auto_renew, State = #s{auto_renew_ms = MS}) ->
|
||||
erlang:send_after(MS, self(), auto_renew),
|
||||
NewState = i_renew(State),
|
||||
{noreply, NewState};
|
||||
handle_info(Unexpected, State) ->
|
||||
ok = log(warning, "~p ~p: unexpected info: ~p", [?MODULE, self(), Unexpected]),
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
%%-------------------------------------------------------------------
|
||||
%% INTERNALS
|
||||
%%-------------------------------------------------------------------
|
||||
|
||||
i_renew(State = #s{base_path = BasePath}) ->
|
||||
NewCache = gh_sfc_cache:new(BasePath),
|
||||
NewState = State#s{cache = NewCache},
|
||||
NewState.
|
||||
90
gex_httpd/src/gh_sfc_cache.erl
Normal file
90
gex_httpd/src/gh_sfc_cache.erl
Normal file
@ -0,0 +1,90 @@
|
||||
% @doc
|
||||
% cache data management.
|
||||
%
|
||||
% Not pure code because logging and spam filesystem. But not a process
|
||||
%
|
||||
% Adapted from FEWD: https://git.qpq.swiss/pharpend/fewd/src/commit/9adbf67ebde14c7c1d8de70ec9b241e6d4ee6f45/src/fd_httpd_sfc_cache.erl
|
||||
-module(gh_sfc_cache).
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
-export_type([
|
||||
cache/0
|
||||
]).
|
||||
|
||||
-export([
|
||||
query/2,
|
||||
new/0, new/1
|
||||
]).
|
||||
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
|
||||
-type cache() :: #{HttpPath :: binary() := Entry :: gh_sfc_entry:entry()}.
|
||||
|
||||
|
||||
-spec query(HttpPath, Cache) -> Result
|
||||
when HttpPath :: binary(),
|
||||
Cache :: cache(),
|
||||
Result :: {found, Entry}
|
||||
| not_found,
|
||||
Entry :: gh_sfc_entry:entry().
|
||||
|
||||
query(HttpPath, Cache) ->
|
||||
case maps:find(HttpPath, Cache) of
|
||||
{ok, Entry} -> {found, Entry};
|
||||
error -> not_found
|
||||
end.
|
||||
|
||||
|
||||
-spec new() -> cache().
|
||||
new() -> #{}.
|
||||
|
||||
|
||||
-spec new(BasePath) -> cache()
|
||||
when BasePath :: file:filename().
|
||||
% @doc
|
||||
% if you give a file path it just takes the parent dir
|
||||
%
|
||||
% recursively crawls through file tree and picks
|
||||
%
|
||||
% IO errors will be logged but will result in cache misses
|
||||
|
||||
new(BasePath) ->
|
||||
case filelib:is_file(BasePath) of
|
||||
true -> new2(BasePath);
|
||||
false ->
|
||||
tell("~p:new(~p): no such file or directory, returning empty cache", [?MODULE, BasePath]),
|
||||
#{}
|
||||
end.
|
||||
|
||||
new2(BasePath) ->
|
||||
BaseDir =
|
||||
case filelib:is_dir(BasePath) of
|
||||
true -> filename:absname(BasePath);
|
||||
false -> filename:absname(filename:dirname(BasePath))
|
||||
end,
|
||||
BBaseDir = unicode:characters_to_binary(BaseDir),
|
||||
HandlePath =
|
||||
fun(AbsPath, AccCache) ->
|
||||
BAbsPath = unicode:characters_to_binary(AbsPath),
|
||||
HttpPath = remove_prefix(BBaseDir, BAbsPath),
|
||||
NewCache =
|
||||
case gh_sfc_entry:new(AbsPath) of
|
||||
{found, Entry} -> maps:put(HttpPath, Entry, AccCache);
|
||||
not_found -> AccCache
|
||||
end,
|
||||
NewCache
|
||||
end,
|
||||
filelib:fold_files(_dir = BaseDir,
|
||||
_match = ".+",
|
||||
_recursive = true,
|
||||
_fun = HandlePath,
|
||||
_init_acc = #{}).
|
||||
|
||||
remove_prefix(Prefix, From) ->
|
||||
Size = byte_size(Prefix),
|
||||
<<Prefix:Size/bytes, Rest/bytes>> = From,
|
||||
Rest.
|
||||
107
gex_httpd/src/gh_sfc_entry.erl
Normal file
107
gex_httpd/src/gh_sfc_entry.erl
Normal file
@ -0,0 +1,107 @@
|
||||
% @doc non-servery functions for static file caching
|
||||
%
|
||||
% library code. Not pure code because logging and spam filesystem. but not a
|
||||
% process
|
||||
%
|
||||
% Adapted from FEWD: https://git.qpq.swiss/pharpend/fewd/src/commit/9adbf67ebde14c7c1d8de70ec9b241e6d4ee6f45/src/fd_httpd_sfc_entry.erl
|
||||
-module(gh_sfc_entry).
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
-export_type([
|
||||
encoding/0,
|
||||
entry/0
|
||||
]).
|
||||
|
||||
-export([
|
||||
%% constructor
|
||||
new/1,
|
||||
%% accessors
|
||||
fs_path/1, last_modified/1, mime_type/1, encoding/1, contents/1
|
||||
]).
|
||||
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
|
||||
%% types
|
||||
|
||||
% id = not compressed
|
||||
-type encoding() :: none | gzip.
|
||||
|
||||
-record(e, {fs_path :: file:filename(),
|
||||
last_modified :: file:date_time(),
|
||||
mime_type :: string(),
|
||||
encoding :: encoding(),
|
||||
contents :: binary()}).
|
||||
|
||||
-opaque entry() :: #e{}.
|
||||
|
||||
%% accessors
|
||||
|
||||
fs_path(#e{fs_path = X}) -> X.
|
||||
last_modified(#e{last_modified = X}) -> X.
|
||||
mime_type(#e{mime_type = X}) -> X.
|
||||
encoding(#e{encoding = X}) -> X.
|
||||
contents(#e{contents = X}) -> X.
|
||||
|
||||
%% API
|
||||
|
||||
-spec new(Path) -> Result
|
||||
when Path :: file:filename(),
|
||||
Result :: {found, entry()}
|
||||
| not_found.
|
||||
% @doc
|
||||
% absolute file path stored in resulting record
|
||||
%
|
||||
% returns not_found if ANY I/O error occurs during the process. will be logged
|
||||
|
||||
new(Path) ->
|
||||
log(info, "~tp:new(~tp)", [?MODULE, Path]),
|
||||
case file:read_file(Path) of
|
||||
{ok, Binary} ->
|
||||
{found, new2(Path, Binary)};
|
||||
Error ->
|
||||
tell("~tp:new(~tp): file read error: ~tp", [?MODULE, Path, Error]),
|
||||
not_found
|
||||
end.
|
||||
|
||||
%% can assume file exists
|
||||
new2(FsPath, FileBytes) ->
|
||||
LastModified = filelib:last_modified(FsPath),
|
||||
{Encoding, MimeType} = mimetype_compress(FsPath),
|
||||
Contents =
|
||||
case Encoding of
|
||||
none -> FileBytes;
|
||||
gzip -> zlib:gzip(FileBytes)
|
||||
end,
|
||||
#e{fs_path = FsPath,
|
||||
last_modified = LastModified,
|
||||
mime_type = MimeType,
|
||||
encoding = Encoding,
|
||||
contents = Contents}.
|
||||
|
||||
mimetype_compress(FsPath) ->
|
||||
case string:casefold(filename:extension(FsPath)) of
|
||||
%% only including the ones i anticipate encountering
|
||||
%% plaintext formats
|
||||
".css" -> {gzip, "text/css"};
|
||||
".htm" -> {gzip, "text/html"};
|
||||
".html" -> {gzip, "text/html"};
|
||||
".js" -> {gzip, "text/javascript"};
|
||||
".json" -> {gzip, "application/json"};
|
||||
".map" -> {gzip, "application/json"};
|
||||
".md" -> {gzip, "text/markdown"};
|
||||
".ts" -> {gzip, "text/x-typescript"};
|
||||
".txt" -> {gzip, "text/plain"};
|
||||
%% binary formats
|
||||
".gif" -> {none, "image/gif"};
|
||||
".jpg" -> {none, "image/jpeg"};
|
||||
".jpeg" -> {none, "image/jpeg"};
|
||||
".mp4" -> {none, "video/mp4"};
|
||||
".png" -> {none, "image/png"};
|
||||
".webm" -> {none, "video/webm"};
|
||||
".webp" -> {none, "image/webp"};
|
||||
_ -> {none, "application/octet-stream"}
|
||||
end.
|
||||
@ -12,11 +12,12 @@
|
||||
%%% @end
|
||||
|
||||
-module(gh_sup).
|
||||
-vsn("0.1.0").
|
||||
-behaviour(supervisor).
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
|
||||
-vsn("0.1.0").
|
||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||
-copyright("2025-2026 QPQ AG").
|
||||
-license("MIT").
|
||||
|
||||
-export([start_link/0]).
|
||||
-export([init/1]).
|
||||
@ -36,11 +37,17 @@ start_link() ->
|
||||
|
||||
init([]) ->
|
||||
RestartStrategy = {one_for_one, 1, 60},
|
||||
StaticFileCache = {gh_sfc,
|
||||
{gh_sfc, start_link, []},
|
||||
permanent,
|
||||
5000,
|
||||
worker,
|
||||
[gh_sfc]},
|
||||
Clients = {gh_clients,
|
||||
{gh_clients, start_link, []},
|
||||
permanent,
|
||||
5000,
|
||||
supervisor,
|
||||
[gh_clients]},
|
||||
Children = [Clients],
|
||||
Children = [StaticFileCache, Clients],
|
||||
{ok, {RestartStrategy, Children}}.
|
||||
|
||||
@ -20,9 +20,9 @@
|
||||
{key_name,none}.
|
||||
{a_email,"peterharpending@qpq.swiss"}.
|
||||
{c_email,"peterharpending@qpq.swiss"}.
|
||||
{copyright,"Peter Harpending"}.
|
||||
{copyright,"2025-2026, QPQ AG"}.
|
||||
{file_exts,[]}.
|
||||
{license,skip}.
|
||||
{license,mit}.
|
||||
{repo_url,"https://git.qpq.swiss/QPQ-AG/gex"}.
|
||||
{tags,[]}.
|
||||
{ws_url,"https://git.qpq.swiss/QPQ-AG/gex"}.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user