aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTrumeet <yuuta@yuuta.moe>2022-07-26 17:17:09 -0700
committerTrumeet <yuuta@yuuta.moe>2022-07-26 17:17:09 -0700
commit8b07bf593e54dd876e30d0cb1c7c7226d0d1b1e2 (patch)
treefa39391ea1eef6e87a563ee417ba709a33dae596
parent219e4bb54375995fa377a1bcf9601a77cf6f1fee (diff)
downloadacron-8b07bf593e54dd876e30d0cb1c7c7226d0d1b1e2.tar
acron-8b07bf593e54dd876e30d0cb1c7c7226d0d1b1e2.tar.gz
acron-8b07bf593e54dd876e30d0cb1c7c7226d0d1b1e2.tar.bz2
acron-8b07bf593e54dd876e30d0cb1c7c7226d0d1b1e2.zip
feat(acronc): add acronc(1), the Acron cli
Still in early development, Windows support is incomplete. Signed-off-by: Trumeet <yuuta@yuuta.moe>
-rw-r--r--client/libacron/CMakeLists.txt25
-rw-r--r--client/libacron/README.md3
-rw-r--r--client/libacron/apps/acronc/README.md11
-rw-r--r--client/libacron/apps/acronc/async_dns.c65
-rw-r--r--client/libacron/apps/acronc/client.c114
-rw-r--r--client/libacron/apps/acronc/client.h12
-rw-r--r--client/libacron/apps/acronc/common.h8
-rw-r--r--client/libacron/apps/acronc/config.c100
-rw-r--r--client/libacron/apps/acronc/config.h12
-rw-r--r--client/libacron/apps/acronc/handler.h55
-rw-r--r--client/libacron/apps/acronc/handler_signal.c40
-rw-r--r--client/libacron/apps/acronc/handler_socket.c255
-rw-r--r--client/libacron/apps/acronc/handler_stdin.c85
-rw-r--r--client/libacron/apps/acronc/helpers.c58
-rw-r--r--client/libacron/apps/acronc/helpers.h24
-rw-r--r--client/libacron/apps/acronc/log.c61
-rw-r--r--client/libacron/apps/acronc/log.h52
-rw-r--r--client/libacron/apps/acronc/main.c105
18 files changed, 1079 insertions, 6 deletions
diff --git a/client/libacron/CMakeLists.txt b/client/libacron/CMakeLists.txt
index b64a299..57ce521 100644
--- a/client/libacron/CMakeLists.txt
+++ b/client/libacron/CMakeLists.txt
@@ -89,19 +89,32 @@ target_link_libraries(ac ${LIBAC_DEPS})
target_include_directories(ac-static PUBLIC ${LIBAC_INCLUDES})
target_link_libraries(ac-static ${LIBAC_DEPS})
+# Apps
+if(WIN32)
+ set(APPS_DEPS ws2_32)
+endif()
+set(APPS_DEPS ${APPS_DEPS} ac)
+
+# apps/helloworld
add_executable(helloworld
apps/helloworld/main.c
apps/helloworld/net.c
apps/helloworld/net.h
)
-if(WIN32)
- set(ACRONC_DEPS ws2_32)
-endif()
-set(ACRONC_DEPS ${ACRON_DEPS} ac)
-target_link_libraries(helloworld ${ACRONC_DEPS})
+target_link_libraries(helloworld ${APPS_DEPS})
target_include_directories(helloworld PUBLIC "${PROJECT_BINARY_DIR}" include/)
-install(TARGETS ac ac-static helloworld
+# apps/acronc
+add_executable(acronc
+ apps/acronc/main.c
+ apps/acronc/common.h
+ apps/acronc/log.h
+ apps/acronc/log.c
+ apps/acronc/handler.h apps/acronc/handler_signal.c apps/acronc/config.c apps/acronc/config.h apps/acronc/client.c apps/acronc/client.h apps/acronc/handler_stdin.c apps/acronc/helpers.c apps/acronc/helpers.h apps/acronc/handler_socket.c apps/acronc/async_dns.c)
+target_link_libraries(acronc ${APPS_DEPS} uv)
+target_include_directories(acronc PUBLIC "${PROJECT_BINARY_DIR}" include/)
+
+install(TARGETS ac ac-static helloworld acronc
EXPORT ${PROJECT_NAME}-targets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
diff --git a/client/libacron/README.md b/client/libacron/README.md
index 4514e21..418e07b 100644
--- a/client/libacron/README.md
+++ b/client/libacron/README.md
@@ -3,6 +3,9 @@
A client library written in C,
based on [json-c](https://github.com/json-c/json-c) and [wic](https://github.com/cjhdev/wic).
+This document is for client developers. For users who want a remote console to their Minecraft
+server, they should consult [acronc(1)](apps/acronc), a ready-to-use Acron cli.
+
## Building
Requirements:
diff --git a/client/libacron/apps/acronc/README.md b/client/libacron/apps/acronc/README.md
new file mode 100644
index 0000000..87c9db0
--- /dev/null
+++ b/client/libacron/apps/acronc/README.md
@@ -0,0 +1,11 @@
+# acronc(1)
+
+Acron client using libac and libuv, written in C.
+
+Current status: In development.
+
+Windows is currently not supported yet.
+
+## License
+
+GPL-2.0-only \ No newline at end of file
diff --git a/client/libacron/apps/acronc/async_dns.c b/client/libacron/apps/acronc/async_dns.c
new file mode 100644
index 0000000..2e221cf
--- /dev/null
+++ b/client/libacron/apps/acronc/async_dns.c
@@ -0,0 +1,65 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#include "handler.h"
+#include "log.h"
+
+#include <uv.h>
+
+static uv_getaddrinfo_t resolv;
+
+static struct addrinfo *ai = NULL;
+static struct addrinfo *ai_current = NULL;
+static void (*cb)(int status,
+ const struct addrinfo *,
+ void (*on_connect_result)(bool)) = NULL;
+
+static void on_conn(bool res) {
+ if (res) {
+ uv_freeaddrinfo(ai);
+ ai = NULL;
+ ai_current = NULL;
+ cb = NULL;
+ return;
+ }
+ ai_current = ai_current->ai_next;
+ if (!ai_current) {
+ /* No more result available. */
+ cb(1, NULL, NULL);
+ uv_freeaddrinfo(ai);
+ ai = NULL;
+ ai_current = NULL;
+ cb = NULL;
+ return;
+ }
+ cb(0, ai_current, &on_conn);
+}
+
+static void on_resolv(uv_getaddrinfo_t *req, int status, struct addrinfo *res) {
+ LOGDV("on_resolv(req = %p): status = %d, res = %p",
+ req,
+ status,
+ res);
+ if (status) {
+ LOGEV("Cannot resolve host: %s", uv_strerror(status));
+ cb(status, NULL, NULL);
+ return;
+ }
+ ai = res;
+ ai_current = res;
+ cb(0, ai_current, &on_conn);
+}
+
+int a_dns(const char *host,
+ uint16_t port,
+ void (*c)(int status,
+ const struct addrinfo *,
+ void (*on_connect_result)(bool))) {
+ int r;
+ char service[6];
+ snprintf(service, 6, "%u", port);
+ cb = c;
+ if ((r = uv_getaddrinfo(loop, &resolv, on_resolv, host, service, NULL))) return r;
+ return 0;
+}
diff --git a/client/libacron/apps/acronc/client.c b/client/libacron/apps/acronc/client.c
new file mode 100644
index 0000000..5553d4c
--- /dev/null
+++ b/client/libacron/apps/acronc/client.c
@@ -0,0 +1,114 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#include "client.h"
+#include "helpers.h"
+
+#include <libac.h>
+#include <stdio.h>
+
+static void handle_event(const ac_event_t *event) {
+ switch (event->type) {
+ case AC_EVENT_LAGGING: {
+ ac_event_lagging_t *o = (ac_event_lagging_t *) event;
+ printf("Server lagging: running %ld milliseconds (%ld ticks) behind.\n",
+ o->ms,
+ o->ticks);
+ break;
+ }
+ case AC_EVENT_ENTITY_DEATH: {
+ ac_event_entity_death_t *o = (ac_event_entity_death_t *) event;
+ printf("Entity '%s' died at %s(%.2f, %.2f, %.2f): %s.\n",
+ o->entity.name,
+ world_name(o->entity.world),
+ o->entity.pos.x,
+ o->entity.pos.y,
+ o->entity.pos.z,
+ o->message);
+ break;
+ }
+ case AC_EVENT_PLAYER_MESSAGE: {
+ ac_event_player_message_t *o = (ac_event_player_message_t *) event;
+ printf("Player '%s' said at %s(%.2f, %.2f, %.2f): %s.\n",
+ o->player.name,
+ world_name(o->player.world),
+ o->player.pos.x,
+ o->player.pos.y,
+ o->player.pos.z,
+ o->text);
+ break;
+ }
+ case AC_EVENT_PLAYER_DISCONNECT: {
+ ac_event_player_disconnect_t *o = (ac_event_player_disconnect_t *) event;
+ printf("Player '%s' disconnected at %s(%.2f, %.2f, %.2f): %s.\n",
+ o->player.name,
+ world_name(o->player.world),
+ o->player.pos.x,
+ o->player.pos.y,
+ o->player.pos.z,
+ o->reason);
+ break;
+ }
+ case AC_EVENT_PLAYER_JOIN: {
+ ac_event_player_join_t *o = (ac_event_player_join_t *) event;
+ printf("Player '%s' joined at %s(%.2f, %.2f, %.2f).\n",
+ o->player.name,
+ world_name(o->player.world),
+ o->player.pos.x,
+ o->player.pos.y,
+ o->player.pos.z);
+ break;
+ }
+ default: {
+ printf("Received an unrecognized event of type '%u'.\n",
+ event->type);
+ }
+ }
+}
+
+static void handle_response(const ac_response_t *response) {
+ switch (response->type) {
+ case AC_RESPONSE_OK: {
+ ac_response_ok_t *o = (ac_response_ok_t *) response;
+ printf("Request %d OK.\n",
+ o->id);
+ break;
+ }
+ case AC_RESPONSE_ERROR: {
+ ac_response_error_t *o = (ac_response_error_t *) response;
+ printf("Request %d failed: %s (%d).\n",
+ o->id,
+ o->message,
+ o->code);
+ break;
+ }
+ case AC_RESPONSE_CMD_OUT: {
+ ac_response_cmd_out_t *o = (ac_response_cmd_out_t *) response;
+ printf("Request %d output by %s: %s.\n",
+ o->id,
+ o->sender,
+ o->out);
+ break;
+ }
+ case AC_RESPONSE_CMD_RESULT: {
+ ac_response_cmd_result_t *o = (ac_response_cmd_result_t *) response;
+ printf("Request %d is done: %s (%d).\n",
+ o->id,
+ o->success ? "Success" : "Failed",
+ o->result);
+ break;
+ }
+ default: {
+ printf("Received an unrecognized response of type '%u'.\n",
+ response->type);
+ }
+ }
+}
+
+int handle_object(ac_obj_t *obj) {
+ if (AC_IS_RESPONSE(obj->type)) handle_response((ac_response_t *) obj);
+ else handle_event((ac_event_t *) obj);
+ ac_object_free(obj);
+ return 0;
+} \ No newline at end of file
diff --git a/client/libacron/apps/acronc/client.h b/client/libacron/apps/acronc/client.h
new file mode 100644
index 0000000..438d7fd
--- /dev/null
+++ b/client/libacron/apps/acronc/client.h
@@ -0,0 +1,12 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#ifndef ACRONC_CLIENT_H
+#define ACRONC_CLIENT_H
+
+#include <libac.h>
+
+int handle_object(ac_obj_t *obj);
+
+#endif /* ACRONC_CLIENT_H */
diff --git a/client/libacron/apps/acronc/common.h b/client/libacron/apps/acronc/common.h
new file mode 100644
index 0000000..0ca3b92
--- /dev/null
+++ b/client/libacron/apps/acronc/common.h
@@ -0,0 +1,8 @@
+/*
+ * Created by yuuta on 7/23/22.
+ */
+
+#ifndef ACRONC_COMMON_H
+#define ACRONC_COMMON_H
+
+#endif /* ACRONC_COMMON_H */
diff --git a/client/libacron/apps/acronc/config.c b/client/libacron/apps/acronc/config.c
new file mode 100644
index 0000000..046d931
--- /dev/null
+++ b/client/libacron/apps/acronc/config.c
@@ -0,0 +1,100 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#include "config.h"
+
+#include <libac.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+
+int config_parse(const int argc, const char **argv, ac_connection_parameters_t *params) {
+ if (argc <= 1) {
+ fprintf(stderr, "Usage: %s [-t token] [-p port] <ID>@<server>\n"
+ "Please refer to acronc(1) for more details.\n",
+ argv[0] ? "acronc" : argv[0]);
+ return 64;
+ }
+ bool svr_set = false;
+ for (int i = 1; i < argc; i++) {
+ const char *arg = argv[i];
+ if (arg[0] != '-') {
+ if (svr_set) {
+ fprintf(stderr, "Unexpected value: %s\n", arg);
+ return 64;
+ }
+ const char *id = strtok((char *) arg, "@");
+ if (!id) {
+ fprintf(stderr, "Malformed argument: should be ID@server.\n");
+ return 64;
+ }
+ const char *server = strtok(NULL, "");
+ if (!server) {
+ fprintf(stderr, "Malformed argument: should be ID@server.\n");
+ return 64;
+ }
+ params->host = (char *) server;
+ params->id = (char *) id;
+ svr_set = true;
+ continue;
+ }
+ char a = arg[1];
+ char **value_ptr;
+ char *port_str = NULL;
+ switch (a) {
+ case 't': {
+ value_ptr = &params->token;
+ goto read_value;
+ }
+ case 'p': {
+ value_ptr = &port_str;
+ goto read_value;
+ }
+ default: {
+ fprintf(stderr, "Unexpected switch -%c.\n", a);
+ return 64;
+ }
+ }
+ read_value:
+ {
+ if (i == (argc - 1)) {
+ fprintf(stderr, "-%c requires an argument.\n", a);
+ return 64;
+ }
+ *value_ptr = (char *) argv[++i];
+ }
+ if (port_str) {
+ errno = 0;
+ char *endptr;
+ unsigned long val = strtol(port_str, &endptr, 0);
+ if (errno) {
+ const int e = errno;
+ fprintf(stderr, "Cannot parse port: %s", strerror(e));
+ return e;
+ }
+ if (endptr == port_str) {
+ fprintf(stderr, "Illegal port.\n");
+ return 64;
+ }
+ if (val > UINT16_MAX) {
+ fprintf(stderr, "Illegal port.\n");
+ return 64;
+ }
+ params->port = (uint16_t) val;
+ port_str = NULL;
+ }
+ }
+ if (!params->host || !params->id) {
+ fprintf(stderr, "Usage: acronc ID@server[:port]\n");
+ return 64;
+ }
+ char tok[1024];
+ if (!params->token) {
+ printf("Token: ");
+ scanf("%1023[^\n]", tok);
+ params->token = tok;
+ }
+ return 0;
+} \ No newline at end of file
diff --git a/client/libacron/apps/acronc/config.h b/client/libacron/apps/acronc/config.h
new file mode 100644
index 0000000..ee51f34
--- /dev/null
+++ b/client/libacron/apps/acronc/config.h
@@ -0,0 +1,12 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#ifndef ACRONC_CONFIG_H
+#define ACRONC_CONFIG_H
+
+#include <libac.h>
+
+int config_parse(int argc, const char **argv, ac_connection_parameters_t *params);
+
+#endif /* ACRONC_CONFIG_H */
diff --git a/client/libacron/apps/acronc/handler.h b/client/libacron/apps/acronc/handler.h
new file mode 100644
index 0000000..1a2ccfe
--- /dev/null
+++ b/client/libacron/apps/acronc/handler.h
@@ -0,0 +1,55 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#ifndef ACRONC_HANDLER_H
+#define ACRONC_HANDLER_H
+
+#include <uv.h>
+#include <libac.h>
+
+enum exit_reason {
+ /* Exit due to a signal. Socket is working. */
+ EXIT_SIGNAL,
+ /* Exit due to an EOF from stdin. Socket is working. */
+ EXIT_STDIN_EOF,
+ /* Exit due to an EOF from socket. Socket is closed. */
+ EXIT_SOCKET_EOF,
+ /* Exit due to a socket failure. Socket is closed. */
+ EXIT_FAILURE_SOCKET,
+ /* Exit due to a failure. Socket is working. */
+ EXIT_FAILURE
+};
+
+struct uv_obj {
+ void *obj;
+ bool running;
+};
+
+extern uv_loop_t *loop;
+
+extern void (*on_exit)(enum exit_reason reason);
+
+int h_signal(void);
+
+int h_stdin(int (*on_input)(ac_request_t *req),
+ void (*on_close)(void));
+
+int h_socket(ac_connection_parameters_t *p,
+ const struct addrinfo *ai,
+ void (*on_connect_result)(bool),
+ int (*on_ready)(void),
+ int (*on_received)(ac_obj_t *obj),
+ void (*on_closed)(void));
+
+int sock_request(ac_request_t *req);
+
+int sock_ext(bool trigger_callback);
+
+int a_dns(const char *host,
+ uint16_t port,
+ void (*on_resolv)(int status,
+ const struct addrinfo *,
+ void (*on_connect_result)(bool)));
+
+#endif /* ACRONC_HANDLER_H */
diff --git a/client/libacron/apps/acronc/handler_signal.c b/client/libacron/apps/acronc/handler_signal.c
new file mode 100644
index 0000000..1d0ba7e
--- /dev/null
+++ b/client/libacron/apps/acronc/handler_signal.c
@@ -0,0 +1,40 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#include "handler.h"
+#include "log.h"
+
+static uv_signal_t sigint;
+static struct uv_obj obj_sigint = {
+ .obj = &sigint,
+ .running = false
+};
+static uv_signal_t sigterm;
+static struct uv_obj obj_sigterm = {
+ .obj = &sigterm,
+ .running = false
+};
+
+static void on_signal(uv_signal_t *handle, int signum) {
+ LOGDV("on_signal(handle = %p): %d",
+ handle,
+ signum);
+ uv_signal_stop(&sigint);
+ obj_sigint.running = false;
+ uv_signal_stop(&sigterm);
+ obj_sigterm.running = false;
+ on_exit(EXIT_SIGNAL);
+}
+
+int h_signal(void) {
+ int r;
+ if ((r = uv_signal_init(loop, &sigint))) return r;
+ if ((r = uv_signal_start(&sigint, on_signal, SIGINT))) return r;
+
+ obj_sigint.running = true;
+ if ((r = uv_signal_init(loop, &sigterm))) return r;
+ if ((r = uv_signal_start(&sigterm, on_signal, SIGTERM))) return r;
+ obj_sigterm.running = true;
+ return 0;
+}
diff --git a/client/libacron/apps/acronc/handler_socket.c b/client/libacron/apps/acronc/handler_socket.c
new file mode 100644
index 0000000..ff8eefb
--- /dev/null
+++ b/client/libacron/apps/acronc/handler_socket.c
@@ -0,0 +1,255 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#include "handler.h"
+#include "log.h"
+#include "helpers.h"
+
+#include <uv.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void (*cb_conn)(bool);
+
+static int (*cb_ready)(void);
+
+static int (*cb_recv)(ac_obj_t *obj);
+
+static void (*cb_close)(void);
+
+static ac_connection_parameters_t *params;
+
+static uv_tcp_t sock;
+static uv_write_t write;
+
+static uv_connect_t conn;
+
+static bool ready = false;
+static void *ac_conn = NULL;
+
+#define RUNNING !uv_is_closing((uv_handle_t *) &sock)
+
+static void on_close(uv_handle_t *handle) {
+ LOGDV("on_close(handle = %p)",
+ handle);
+ if (handle->data)
+ cb_close();
+}
+
+static void ex2(bool force, bool cb) {
+ LOGDV("Exiting socket handlers: Force WebSocket close: %s; Socket still running: %s.",
+ force ? "true" : "false",
+ RUNNING ? "true" : "false");
+ if (ac_conn) {
+ ac_disconnect(ac_conn,
+ RUNNING ? force : true /* If sock is not running, always force */);
+ ac_conn = NULL;
+ }
+ if (RUNNING) {
+ sock.data = cb ? &ex2 /* Any non-NULL value */ : NULL;
+ uv_close((uv_handle_t *) &sock, on_close);
+ } else {
+ if (cb) {
+ cb_close();
+ }
+ }
+}
+
+static void ex(bool force) {
+ ex2(force, true);
+}
+
+int sock_ext(bool trigger_callback) {
+ ex2(false, trigger_callback);
+ return 0;
+}
+
+static void on_alloc(uv_handle_t *handle, size_t size, uv_buf_t *buf) {
+ LOGDV("on_alloc(handle = %p): size = %lu",
+ handle,
+ size);
+ void *b = malloc(size);
+ if (!b) {
+ LOGEV("Cannot allocate memory of %u bytes: %s.",
+ size,
+ strerror(errno));
+ /* Socket is still working now. */
+ ex(false);
+ *buf = uv_buf_init(NULL, 0); /* Just in case? */
+ return;
+ }
+ *buf = uv_buf_init(b, size);
+}
+
+static void on_read(uv_stream_t *tcp, ssize_t nread, const uv_buf_t *buf) {
+ LOGDV("on_read(stream = %p): nread = %ld, buf = %p, buf->base = %p, buf->len = %lu",
+ tcp,
+ nread,
+ buf, buf->base, buf->len);
+ if (!nread) {
+ /* EAGAIN */
+ if (buf->base) free(buf->base);
+ return;
+ }
+ if (nread == UV_EOF) {
+ LOGW("Received EOF from server.");
+ free(buf->base);
+ /* The socket * should * be closed already? */
+ ex(true);
+ return;
+ }
+ if (nread < 0) {
+ LOGEV("Encountered a failure while reading from the socket: ", uv_strerror(nread));
+ if (buf->base) free(buf->base);
+ /* Docs: The callee is responsible for stopping/closing the stream when an error happens */
+ ex(true); /* Force close libac connection here. */
+ return;
+ }
+ int r;
+ ac_obj_t *obj = NULL;
+ if ((r = ac_receive(ac_conn, buf->base, nread, &obj))) {
+ LOGEV("Cannot parse the response (%d).", r);
+ /* libac error. Socket is working. */
+ ex(false);
+ return;
+ }
+
+ if (!ready) {
+ enum ac_connection_state state;
+ if ((r = ac_get_state(ac_conn, &state))) {
+ LOGEV("Cannot get state (%d).", r);
+ /* libac error. Socket is working. */
+ ex(false);
+ return;
+ }
+ if (state == AC_STATE_READY) {
+ ready = true;
+ if ((cb_ready())) {
+ /* acronc error. Socket is working. */
+ ex(false);
+ return;
+ }
+ }
+ }
+ if (obj) {
+ LOGDV("Got object: %p", obj);
+ /* uv_async_send is unreliable, and missed messages will cause memory leak. */
+ if (cb_recv(obj)) {
+ /* acronc error. Socket is working. */
+ ex(false);
+ }
+ }
+
+ free(buf->base);
+}
+
+static void on_write(uv_write_t *req, int status) {
+ LOGDV("on_write(req = %p): %d",
+ req,
+ status);
+ if (status) {
+ LOGEV("Cannot write to socket: %s", uv_strerror(status));
+ /* Socket may be closed? Anyway writing again will definitely be problematic, but closing may not. */
+ ex(true);
+ }
+}
+
+static int on_send(const void *s,
+ const void *buf,
+ const size_t len) {
+ LOGDV("on_send(s = %p): len = %u",
+ s,
+ len);
+ const uv_stream_t *stream = s;
+ uv_buf_t buffer[] = {
+ {.base = (char *) buf, .len = len}
+ };
+ int r;
+ if ((r = uv_write(&write, (uv_stream_t *) stream, buffer, 1, on_write))) {
+ LOGEV("Cannot write to socket: %s", uv_strerror(r));
+ /* Socket may be closed? Anyway writing again will definitely be problematic, but closing may not. */
+ ex(true);
+ }
+ return 0;
+}
+
+static void on_retry_closed(uv_handle_t *handle) {
+ cb_conn(false);
+}
+
+static void on_connect(uv_connect_t *req, int status) {
+ LOGDV("on_connect(req = %p): status = %d",
+ req,
+ status);
+ if (status) {
+ LOGEV("Cannot connect to the server: %s", uv_strerror(status));
+ /* Close it for the next retry. */
+ if (RUNNING) {
+ LOGD("Closing socket before retry.");
+ uv_close((uv_handle_t *) &sock, on_retry_closed);
+ } else {
+ cb_conn(false);
+ }
+ return;
+ }
+ LOGI("Connected.");
+ uv_stream_t *stream = req->handle;
+ int r;
+ params->sock = stream;
+ if ((r = ac_connect(*params, &ac_conn))) {
+ LOGEV("Cannot initialize connection: %d.", r);
+ cb_conn(false);
+ return;
+ }
+ if ((r = uv_read_start(stream, on_alloc, on_read))) {
+ LOGEV("Cannot read socket: %s", uv_strerror(r));
+ cb_conn(false);
+ return;
+ }
+ cb_conn(true);
+}
+
+int h_socket(ac_connection_parameters_t *p,
+ const struct addrinfo *ai,
+ void (*on_connect_result)(bool),
+ int (*on_ready)(void),
+ int (*on_received)(ac_obj_t *obj),
+ void (*on_closed)(void)) {
+ cb_conn = on_connect_result;
+ cb_ready = on_ready;
+ cb_recv = on_received;
+ cb_close = on_closed;
+ params = p;
+ params->on_send = on_send;
+
+ struct sockaddr *sa = ai->ai_addr;
+ LOGIV("Connecting to %s...", ntop(sa));
+ int r;
+ if ((r = uv_tcp_init(loop, &sock))) {
+ LOGEV("Cannot initialize the socket: %s", uv_strerror(r));
+ return r;
+ }
+ if ((r = uv_tcp_connect(&conn, &sock, sa, on_connect))) {
+ LOGEV("Cannot initialize the connection: %s", uv_strerror(r));
+ return r;
+ }
+ return 0;
+}
+
+int sock_request(ac_request_t *req) {
+ if (!ac_conn) {
+ /* Just in case. */
+ LOGE("sock_request() called on a closed socket.");
+ /* Ignore it. */
+ return 0;
+ }
+ int r = ac_request(ac_conn, req);
+ if (r) {
+ LOGEV("Cannot send request: %d.", r);
+ /* libac error. Socket is working. */
+ ex(false);
+ }
+ return r;
+}
+
diff --git a/client/libacron/apps/acronc/handler_stdin.c b/client/libacron/apps/acronc/handler_stdin.c
new file mode 100644
index 0000000..3ac307d
--- /dev/null
+++ b/client/libacron/apps/acronc/handler_stdin.c
@@ -0,0 +1,85 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#include "handler.h"
+#include "log.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+static unsigned int id = 0;
+
+static uv_tty_t tty;
+
+static int (*cb_recv)(ac_request_t *req);
+
+static void (*cb_close)(void);
+
+static void on_close(uv_handle_t *handle) {
+ cb_close();
+}
+
+static void on_alloc(uv_handle_t *handle, size_t size, uv_buf_t *buf) {
+ LOGDV("on_alloc(handle = %p): size = %lu",
+ handle,
+ size);
+ void *b = malloc(size);
+ if (!b) {
+ LOGEV("Cannot allocate memory of %u bytes: %s.",
+ size,
+ strerror(errno));
+ /* Stream is still working now. */
+ uv_close(handle, on_close);
+ *buf = uv_buf_init(NULL, 0); /* Just in case? */
+ return;
+ }
+ *buf = uv_buf_init(b, size);
+}
+
+static void on_stdin(uv_stream_t *t, ssize_t nread, const uv_buf_t *buf) {
+ LOGDV("on_stdin(stream = %p): nread = %ld, buf = %p, buf->base = %p, buf->len = %lu",
+ t,
+ nread,
+ buf, buf->base, buf->len);
+ if (!nread) {
+ if (buf->base) free(buf->base);
+ return;
+ }
+ if (nread == UV_EOF) {
+ LOGI("Exit.");
+ free(buf->base);
+
+ cb_close();
+ return;
+ }
+ if (nread < 0) {
+ LOGEV("Encountered a failure while reading stdin: ", uv_strerror(nread));
+ if (buf->base) free(buf->base);
+ uv_close((uv_handle_t *) t, on_close);
+ return;
+ }
+ buf->base[nread - 1] = '\0'; /* Remove junk and tailing \n */
+ if ((++ id) > INT_MAX) {
+ id = 0;
+ }
+ ac_request_cmd_t req = {
+ .type = AC_REQUEST_CMD,
+ .id = (int) id,
+ .cmd = buf->base
+ };
+ if (cb_recv((ac_request_t *) &req)) {
+ uv_close((uv_handle_t *) t, on_close);
+ }
+ free(buf->base);
+}
+
+int h_stdin(int (*on_input)(ac_request_t *req),
+ void (*on_close)(void)) {
+ cb_recv = on_input;
+ cb_close = on_close;
+ int r;
+ if ((r = uv_tty_init(loop, &tty, 0, 0))) return r;
+ if ((r = uv_read_start((uv_stream_t *) &tty, on_alloc, on_stdin))) return r;
+ return 0;
+} \ No newline at end of file
diff --git a/client/libacron/apps/acronc/helpers.c b/client/libacron/apps/acronc/helpers.c
new file mode 100644
index 0000000..9e141d4
--- /dev/null
+++ b/client/libacron/apps/acronc/helpers.c
@@ -0,0 +1,58 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#include "helpers.h"
+
+#include <libac.h>
+#include <string.h>
+#include <errno.h>
+
+#ifdef WIN32
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#include <ws2ipdef.h>
+
+#define errno_sock WSAGetLastError()
+
+#else
+
+#include <arpa/inet.h>
+#include <sys/socket.h>
+
+#define errno_sock errno
+
+#endif
+
+const char *ntop(const struct sockaddr *sa) {
+ static char buf[INET6_ADDRSTRLEN];
+ const char *retval;
+ switch (sa->sa_family) {
+ case AF_INET: {
+ retval = inet_ntop(AF_INET, &((struct sockaddr_in *) sa)->sin_addr, buf, sizeof(buf));
+ break;
+ }
+ case AF_INET6: {
+ retval = inet_ntop(AF_INET6, &((struct sockaddr_in6 *) sa)->sin6_addr, buf, sizeof(buf));
+ break;
+ }
+ default: {
+ return "Unknown address family";
+ }
+ }
+ if (retval) return retval;
+ return strerror(errno_sock);
+}
+
+const char *world_name(const enum ac_world world) {
+ switch (world) {
+ case overworld:
+ return "overworld";
+ case nether:
+ return "nether";
+ case end:
+ return "end";
+ default:
+ return "unknown world";
+ }
+}
diff --git a/client/libacron/apps/acronc/helpers.h b/client/libacron/apps/acronc/helpers.h
new file mode 100644
index 0000000..e983439
--- /dev/null
+++ b/client/libacron/apps/acronc/helpers.h
@@ -0,0 +1,24 @@
+/*
+ * Created by yuuta on 7/24/22.
+ */
+
+#ifndef ACRONC_HELPERS_H
+#define ACRONC_HELPERS_H
+
+#include <libac.h>
+
+#ifdef WIN32
+#include <winsock2.h>
+#include <ws2tcpip.h>
+
+#else
+
+#include <sys/socket.h>
+
+#endif
+
+const char *ntop(const struct sockaddr *sa);
+
+const char *world_name(enum ac_world world);
+
+#endif /* ACRONC_HELPERS_H */
diff --git a/client/libacron/apps/acronc/log.c b/client/libacron/apps/acronc/log.c
new file mode 100644
index 0000000..08f48b6
--- /dev/null
+++ b/client/libacron/apps/acronc/log.c
@@ -0,0 +1,61 @@
+/*
+ * Created by yuuta on 1/1/22.
+ */
+
+#define _GNU_SOURCE
+
+#include "log.h"
+#include "config.h"
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#ifdef __linux__
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/syscall.h>
+#endif
+
+void g_log(enum log_level level,
+ const char *file,
+ int line,
+ const char *format,
+ ...) {
+ FILE *stream = stderr;
+ switch (level) {
+ case log_fetal:
+ fprintf(stream, "F");
+ break;
+ case log_error:
+ fprintf(stream, "E");
+ break;
+ case log_warn:
+ fprintf(stream, "W");
+ break;
+ case log_info:
+ fprintf(stream, "I");
+ break;
+ case log_debug:
+#ifdef DEBUG
+ fprintf(stream, "D");
+ break;
+#else
+ return;
+#endif
+ default:
+ fprintf(stderr, "Unknown log level: %d.\n", level);
+ assert(0);
+ }
+ int tid = -1;
+#ifdef __linux__
+ tid = (int) syscall(__NR_gettid);
+#endif
+ fprintf(stream, "[%d %s:%d]: ",
+ tid, file, line);
+ va_list list;
+ va_start(list, format);
+ vfprintf(stream, format, list);
+ va_end(list);
+ fprintf(stream, "\n");
+}
diff --git a/client/libacron/apps/acronc/log.h b/client/libacron/apps/acronc/log.h
new file mode 100644
index 0000000..7bc9532
--- /dev/null
+++ b/client/libacron/apps/acronc/log.h
@@ -0,0 +1,52 @@
+/*
+ * Created by yuuta on 1/1/22.
+ */
+
+#ifndef LOG_H
+#define LOG_H
+
+enum log_level {
+ log_fetal = 1,
+ log_error = 2,
+ log_warn = 3,
+ log_info = 4,
+ log_debug = 5
+};
+
+void g_log(enum log_level level,
+ const char *file,
+ int line,
+ const char *format,
+ ...);
+
+#define LOGF(X) g_log(log_fetal, __FUNCTION__, __LINE__, X)
+
+#define LOGFV(X, ...) g_log(log_fetal, __FUNCTION__, __LINE__, X, __VA_ARGS__)
+
+#define LOGE(X) g_log(log_error, __FUNCTION__, __LINE__, X)
+
+#define LOGEV(X, ...) g_log(log_error, __FUNCTION__, __LINE__, X, __VA_ARGS__)
+
+#define LOGW(X) g_log(log_warn, __FUNCTION__, __LINE__, X)
+
+#define LOGWV(X, ...) g_log(log_warn, __FUNCTION__, __LINE__, X, __VA_ARGS__)
+
+#define LOGI(X) g_log(log_info, __FUNCTION__, __LINE__, X)
+
+#define LOGIV(X, ...) g_log(log_info, __FUNCTION__, __LINE__, X, __VA_ARGS__)
+
+#ifdef DEBUG
+
+#define LOGD(X) g_log(log_debug, __FUNCTION__, __LINE__, X)
+
+#define LOGDV(X, ...) g_log(log_debug, __FUNCTION__, __LINE__, X, __VA_ARGS__)
+
+#else
+
+#define LOGD(X)
+
+#define LOGDV(X, ...)
+
+#endif
+
+#endif /* LOG_H */
diff --git a/client/libacron/apps/acronc/main.c b/client/libacron/apps/acronc/main.c
new file mode 100644
index 0000000..6e18ed2
--- /dev/null
+++ b/client/libacron/apps/acronc/main.c
@@ -0,0 +1,105 @@
+/*
+ * Created by yuuta on 7/23/22.
+ */
+
+#include "log.h"
+#include "handler.h"
+#include "config.h"
+#include "helpers.h"
+#include "client.h"
+
+#include <libac.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <uv.h>
+#include <err.h>
+#include <string.h>
+
+static uv_loop_t lop;
+uv_loop_t *loop = &lop;
+
+static void on_close(uv_handle_t *handle);
+
+static ac_connection_parameters_t params = {
+ .sock = NULL,
+ .version = 0,
+ .port = 25575,
+ .host = NULL,
+ .id = NULL,
+ .token = NULL,
+ .on_send = NULL,
+};
+
+static void cleanup(void) {
+ LOGD("cleanup()");
+ /* sock must be closed before cleanup(). */
+ uv_loop_close(loop);
+ ac_free();
+ uv_tty_reset_mode();
+}
+
+static void on_stdin_closed(void) {
+ sock_ext(true);
+}
+
+static void on_sock_closed(void) {
+ uv_stop(loop);
+}
+
+static int on_input(ac_request_t *req) {
+ /* The socket will close itself upon errors. So do the input stream. */
+ return sock_request(req);
+}
+
+static int on_sock_ready(void) {
+ return h_stdin(on_input, on_stdin_closed);
+}
+
+static void on_resolv(int status, const struct addrinfo *ai, void (*on_connected)(bool)) {
+ if (status) {
+ uv_stop(loop);
+ return;
+ }
+
+ if (h_socket(&params,
+ ai,
+ on_connected,
+ on_sock_ready,
+ handle_object,
+ on_sock_closed)) {
+ /* Mark as true to clean up resources and prevent next retry. */
+ on_connected(true);
+ uv_stop(loop);
+ }
+}
+
+int main(int argc, const char **argv) {
+ int r;
+
+ if ((r = config_parse(argc, argv, &params))) return r;
+ atexit(cleanup);
+ libac_config_t config = {
+#ifdef DEBUG
+ .out = NULL,
+#else
+ .out = NULL,
+#endif
+ .tok = NULL
+ };
+ if ((r = ac_init(&config))) errx(r, "Cannot initialize Acron library.");
+
+ if ((r = uv_loop_init(loop))) goto uviniterr;
+ if ((r = h_signal())) goto uviniterr;
+ if ((r = a_dns(params.host, params.port, on_resolv))) goto uviniterr;
+
+ if ((r = uv_run(loop, UV_RUN_DEFAULT))) {
+ if (r == 1) {
+ /* Seems to return 1 if uv_stop is called. */
+ return 0;
+ }
+ errx(-r, "Cannot run: %s", uv_strerror(r));
+ }
+ return 0;
+ uviniterr:
+ errx(-r, "Cannot initialize: %s", uv_strerror(r));
+} \ No newline at end of file